Wskaźnik C do deklaracji tablicowej z bitowym i operatorem

9

Chcę zrozumieć następujący kod:

//...
#define _C 0x20
extern const char *_ctype_;
//...
__only_inline int iscntrl(int _c)
{
    return (_c == -1 ? 0 : ((_ctype_ + 1)[(unsigned char)_c] & _C));
}

Pochodzi z pliku ctype.h z kodu źródłowego systemu operacyjnego obenbsd. Ta funkcja sprawdza, czy znak jest znakiem kontrolnym, czy drukowaną literą w zakresie ascii. Oto mój obecny ciąg myśli:

  1. Wywoływana jest funkcja iscntrl („a”), a „a” jest konwertowane na wartość całkowitą
  2. najpierw sprawdź, czy _c ma wartość -1, a następnie zwróć 0 w przeciwnym razie ...
  3. zwiększ adres niezdefiniowanego wskaźnika o 1
  4. zadeklaruj ten adres jako wskaźnik do tablicy o długości (znak bez znaku) ((int) „a”)
  5. zastosuj bitowy i operator do _C (0x20) i tablicy (???)

Co dziwne, działa i za każdym razem, gdy zwracane jest 0, dany znak _c nie jest znakiem do wydrukowania. W przeciwnym razie, gdy będzie można go wydrukować, funkcja po prostu zwraca wartość całkowitą, która nie jest przedmiotem szczególnego zainteresowania. Mój problem ze zrozumieniem znajduje się w kroku 3, 4 (trochę) i 5.

Dziękuję za wszelką pomoc.

accentWool
źródło
1
_ctype_jest zasadniczo tablicą masek bitowych. Jest indeksowany według charakteru zainteresowania. Więc _ctype_['A']zawierałby bity odpowiadające „alfa” i „wielkimi literami”, _ctype_['a']zawierałby bity odpowiadające „alfa” i „małymi literami”, _ctype_['1']zawierałby bit odpowiadający „cyfrze” itp. Wygląda na 0x20to, że jest to bit odpowiadający „kontroli” . Ale z jakiegoś powodu _ctype_tablica jest przesunięta o 1, więc bity dla 'a'są naprawdę w _ctype_['a'+1]. (Prawdopodobnie miało to pozwolić, aby działało EOFnawet bez dodatkowego testu.)
Steve Summit
Obsada (unsigned char)polega na tym, aby postacie były podpisane i negatywne.
Steve Summit

Odpowiedzi:

3

_ctype_wydaje się być ograniczoną wewnętrzną wersją tabeli symboli i domyślam się + 1, że nie zadali sobie trudu zapisania jej indeksu 0, ponieważ nie można jej wydrukować. Lub być może używają tabeli 1-indeksowanej zamiast 0-indeksowanej, co jest niestandardowe w C.

Standard C dyktuje to dla wszystkich funkcji ctype.h:

We wszystkich przypadkach argumentem jest an int, którego wartość powinna być reprezentowana jako unsigned charlub równa wartości makraEOF

Przeglądając kod krok po kroku:

  • int iscntrl(int _c)Te inttypy są naprawdę znaków, ale wszystkie funkcje ctype.h są zobowiązani do uchwytu EOF, więc muszą być int.
  • Sprawdzenie przeciw -1jest sprawdzeniem przeciw EOF, ponieważ ma wartość -1.
  • _ctype+1 jest arytmetyką wskaźnika, aby uzyskać adres elementu tablicy.
  • [(unsigned char)_c]jest po prostu dostępem do tablicy tej tablicy, gdzie rzutowanie ma na celu wymuszenie standardowego wymogu reprezentacji parametru jako unsigned char. Zauważ, że charfaktycznie może mieć wartość ujemną, więc jest to programowanie defensywne. Wynikiem []dostępu do tablicy jest pojedynczy znak z wewnętrznej tablicy symboli.
  • &Maskowanie jest tam, aby uzyskać pewną grupę znaków z tablicy symboli. Najwyraźniej wszystkie znaki z ustawionym bitem 5 (maska ​​0x20) są znakami kontrolnymi. Nie ma sensu tego bez oglądania stołu.
  • Wszystko z ustawionym bitem 5 zwróci wartość zamaskowaną 0x20, która jest wartością niezerową. Oznacza to, że funkcja zwraca wartość niezerową w przypadku wartości logicznej true.
Lundin
źródło
Niepoprawne jest, że rzutowanie spełnia standardowe wymaganie, aby wartość była reprezentowalna jako unsigned char. Standard wymaga, aby wartość * była reprezentowana jako unsigned charlub równa EOF, gdy wywoływana jest procedura. Rzutowanie służy jedynie jako „defensywne” programowanie: Korygowanie błędu programisty, który przechodzi znak char(lub a signed char), kiedy na nich ciążył, aby przekazać unsigned charwartość przy użyciu ctype.hmakra. Należy zauważyć, że nie może to poprawić błędu, gdy charwartość -1 jest przekazywana w implementacji, która używa -1 EOF.
Eric Postpischil
To także wyjaśnia + 1. Jeśli makro nie zawierało wcześniej tej korekty obronnej, mogło być zaimplementowane jedynie jako ((_ctype_+1)[_c] & _C), mając w ten sposób tabelę indeksowaną wartościami wstępnej korekty od -1 do 255. Tak więc pierwszy wpis nie został pominięty i służył celowi. Gdy ktoś później dodał rzut obronny, EOFwartość -1 nie działałaby z tym rzutem, więc dodali operator warunkowy, aby potraktować go specjalnie.
Eric Postpischil
3

_ctype_jest wskaźnikiem do globalnej tablicy 257 bajtów. Nie wiem do czego _ctype_[0]służy. _ctype_[1]przez _ctype_[256]_reprezentuje kategorie znaków odpowiednio 0,…, 255: _ctype_[c + 1]reprezentuje kategorię znaku c. Jest to to samo, co powiedzenie, że _ctype_ + 1wskazuje na tablicę 256 znaków, gdzie (_ctype_ + 1)[c]reprezentuje kategorię znaku c.

(_ctype_ + 1)[(unsigned char)_c]nie jest deklaracją. Jest to wyrażenie wykorzystujące operator indeksu tablicy. Uzyskuje dostęp do pozycji (unsigned char)_ctablicy, która zaczyna się od (_ctype_ + 1).

Kod rzutujący _cz intna unsigned charnie jest absolutnie konieczny: funkcje ctype przyjmują wartości char rzutowane na unsigned char( charjest podpisane na OpenBSD): poprawne wywołanie to char c; … iscntrl((unsigned char)c). Mają tę zaletę, że gwarantują, że nie nastąpi przepełnienie bufora: jeśli aplikacja wywoła iscntrlwartość, która jest poza zakresem unsigned chari nie jest -1, funkcja zwraca wartość, która może nie mieć znaczenia, ale przynajmniej nie spowoduje awaria lub wyciek prywatnych danych, które znalazły się pod adresem poza granicami tablicy. Wartość jest nawet poprawna, jeśli funkcja jest wywoływana, char c; … iscntrl(c)dopóki cnie jest -1.

Powodem specjalnego przypadku z -1 jest to, że tak jest EOF. Wiele standardowych funkcji C, które działają na charprzykład getchar, reprezentują znak jako intwartość, która jest wartością char zawiniętą w dodatni zakres i używają specjalnej wartości, EOF == -1aby wskazać, że żaden znak nie może zostać odczytany. Dla funkcji, takich jak getchar, EOFwskazuje koniec pliku, stąd nazwa e nd- O F F Ile. Eric Postpischil sugeruje, że kod był pierwotnie sprawiedliwy return _ctype_[_c + 1]i prawdopodobnie ma rację: _ctype_[0]byłby wartością EOF. Ta prostsza implementacja powoduje przepełnienie bufora, jeśli funkcja jest niewłaściwie używana, podczas gdy bieżąca implementacja pozwala tego uniknąć, jak omówiono powyżej.

Jeśli vwartość znajduje się w tablicy, v & _Csprawdza, czy 0x20ustawiony jest bit at v. Wartości w tablicy są maskami kategorii, w których znajduje się znak: _Cjest ustawiony dla znaków kontrolnych, _Ujest ustawiony na wielkie litery itp.

Gilles „SO- przestań być zły”
źródło
(_ctype_ + 1)[_c] użyłby poprawnego indeksu tablicowego określonego przez standard C, ponieważ to użytkownik jest odpowiedzialny za przekazanie jednej EOFlub unsigned charwartości. Zachowanie innych wartości nie jest zdefiniowane w standardzie C. Obsada nie służy do implementacji zachowania wymaganego przez standard C. Jest to obejście wprowadzone w celu ochrony przed błędami spowodowanymi przez programistów niepoprawnie przekazujących ujemne wartości znaków. Jednak jest niekompletny lub niepoprawny (i nie można go poprawić), ponieważ wartość -1 będzie koniecznie traktowana jako EOF.
Eric Postpischil
To także wyjaśnia + 1. Jeśli makro nie zawierało wcześniej tej korekty obronnej, mogło być zaimplementowane jedynie jako ((_ctype_+1)[_c] & _C), mając w ten sposób tabelę indeksowaną wartościami wstępnej korekty od -1 do 255. Tak więc pierwszy wpis nie został pominięty i służył celowi. Gdy ktoś później dodał rzut obronny, EOFwartość -1 nie działałaby z tym rzutem, więc dodali operator warunkowy, aby potraktować go specjalnie.
Eric Postpischil
2

Zacznę od kroku 3:

zwiększ adres niezdefiniowanego wskaźnika o 1

Wskaźnik nie jest niezdefiniowany. Jest to zdefiniowane w innej jednostce kompilacyjnej. Tak externmówi część kompilatorowi. Kiedy wszystkie pliki zostaną połączone, linker rozpozna odniesienia do niego.

Więc na co to wskazuje?

Wskazuje tablicę z informacjami o każdym znaku. Każda postać ma swój własny wpis. Wpis jest bitmapową reprezentacją cech postaci. Na przykład: jeśli ustawiony jest bit 5, oznacza to, że znak jest znakiem kontrolnym. Kolejny przykład: jeśli ustawiony jest bit 0, oznacza to, że znak jest znakiem wyższym.

Więc coś takiego (_ctype_ + 1)['x']uzyska cechy, które dotyczą 'x'. Następnie wykonywana jest bitowa i sprawdzana, czy bit 5 jest ustawiony, tzn. Sprawdza, czy jest to znak kontrolny.

Przyczyną dodania 1 jest prawdopodobnie to, że rzeczywisty indeks 0 jest zarezerwowany do jakiegoś specjalnego celu.

4386427
źródło
1

Wszystkie informacje tutaj oparte są na analizie kodu źródłowego (i doświadczenia w programowaniu).

Deklaracja

extern const char *_ctype_;

informuje kompilator, że istnieje wskaźnik do const charmiejsca o nazwie _ctype_.

(4) Ten wskaźnik jest dostępny jako tablica.

(_ctype_ + 1)[(unsigned char)_c]

Rzutowanie (unsigned char)_czapewnia, że ​​wartość indeksu mieści się w zakresie unsigned char(0..255).

Arytmetyka wskaźnika _ctype_ + 1skutecznie przesuwa pozycję tablicy o 1 element. Nie wiem, dlaczego zaimplementowali tablicę w ten sposób. Korzystanie z zakresu_ctype_[1] .. _ctype[256]dla wartości znaków 0.. 255pozostawia wartość _ctype_[0]nieużywaną dla tej funkcji. (Przesunięcie 1 można zaimplementować na kilka alternatywnych sposobów.)

Dostęp do tablicy pobiera wartość (typu char, aby zaoszczędzić miejsce), używając wartości znaku jako indeksu tablicy.

(5) Bitowa operacja AND wyodrębnia jeden bit z wartości.

Najwyraźniej wartość z tablicy jest używana jako pole bitowe, w którym bit 5 (licząc od 0 rozpoczynając co najmniej znaczący bit = 0x20) jest flagą dla „jest znakiem kontrolnym”. Tak więc tablica zawiera wartości pól bitowych opisujące właściwości znaków.

Bodo
źródło
Myślę, że przesunęli + 1wskaźnik do wskaźnika, aby wyjaśnić, że 1..256zamiast tego uzyskują dostęp do elementów 1..255,0. _ctype_[1 + (unsigned char)_c]byłby równoważny z powodu niejawnej konwersji na int. I _ctype_[(_c & 0xff) + 1]byłoby jeszcze bardziej jasne i zwięzłe.
cmaster
0

Kluczem tutaj jest zrozumienie, co (_ctype_ + 1)[(unsigned char)_c]robi wyrażenie (które jest następnie podawane do bitowego i operacji, & 0x20aby uzyskać wynik!

Krótka odpowiedź: zwraca element _c + 1tablicy wskazany przez_ctype_ .

W jaki sposób?

Po pierwsze, chociaż wydaje ci się, że uważasz, że _ctype_jest niezdefiniowany, tak naprawdę nie jest! Nagłówek deklaruje go jako zmienną zewnętrzną - ale jest zdefiniowany w (prawie na pewno) jednej z bibliotek wykonawczych, z którymi program jest powiązany podczas jego tworzenia.

Aby zilustrować, w jaki sposób składnia odpowiada indeksowaniu tablic, spróbuj przepracować (a nawet skompilować) następujący krótki program:

#include <stdio.h>
int main() {
    // Code like the following two lines will be defined somewhere in the run-time
    // libraries with which your program is linked, only using _ctype_ in place of _qlist_ ...
    const char list[] = "abcdefghijklmnopqrstuvwxyz";
    const char* _qlist_ = list;
    // These two lines show how expressions like (a)[b] and (a+1)[b] just boil down to
    // a[b] and a[b+1], respectively ...
    char p = (_qlist_)[6];
    char q = (_qlist_ + 1)[6];
    printf("p = %c  q = %c\n", p, q);
    return 0;
}

Poproś o dodatkowe wyjaśnienia i / lub wyjaśnienia.

Adrian Mole
źródło
0

Funkcje zadeklarowane w ctype.hobiektach typu accept int. W przypadku znaków używanych jako argumenty zakłada się, że są one wstępnie rzutowane na typunsigned char . Ten znak jest używany jako indeks w tabeli, która określa jego charakterystykę.

Wygląda na to, że kontrola _c == -1jest używana w przypadku, gdy _czawiera wartość EOF. Jeśli nie, EOF_c jest rzutowany na typ bez znaku, który jest używany jako indeks w tabeli wskazywanej przez wyrażenie _ctype_ + 1. A jeśli bit określony przez maskę 0x20jest ustawiony, to znak jest symbolem kontrolnym.

Aby zrozumieć wyrażenie

(_ctype_ + 1)[(unsigned char)_c]

weź pod uwagę, że indeksowanie tablicy jest operatorem postfiksowym, który jest zdefiniowany jak

postfix-expression [ expression ]

Nie możesz tak pisać

_ctype_ + 1[(unsigned char)_c]

ponieważ to wyrażenie jest równoważne z

_ctype_ + ( 1[(unsigned char)_c] )

Tak więc wyrażenie _ctype_ + 1jest ujęte w nawiasy, aby uzyskać wyrażenie podstawowe.

Tak naprawdę masz

pointer[integral_expression]

który daje obiekt tablicy o indeksie, który jest obliczany jako wyrażenie, w integral_expressionktórym znajduje się wskaźnik (_ctype_ + 1)(gere jest używany jako wskaźnik arytmetuc) i integral_expressionktóry jest indeksem jest wyrażeniem (unsigned char)_c.

Vlad z Moskwy
źródło