Ta układanka składa się z trzech elementów.
Po pierwsze, białe znaki w C i C ++ zwykle nie mają znaczenia poza oddzieleniem sąsiednich tokenów, które w przeciwnym razie są nie do odróżnienia.
Na etapie przetwarzania wstępnego tekst źródłowy jest dzielony na sekwencję tokenów - identyfikatorów, znaków przestankowych, literałów numerycznych, literałów łańcuchowych itp. Ta sekwencja tokenów jest później analizowana pod kątem składni i znaczenia. Tokenizer jest „chciwy” i zbuduje najdłuższy ważny token, jaki jest możliwy. Jeśli napiszesz coś w stylu
inttest;
tokenizer widzi tylko dwa tokeny - identyfikator, inttestpo którym następuje znak interpunkcyjny ;. Na inttym etapie nie rozpoznaje się jako oddzielnego słowa kluczowego (dzieje się to później w procesie). Tak więc, aby wiersz był odczytywany jako deklaracja liczby całkowitej o nazwie test, musimy użyć białych znaków do oddzielenia tokenów identyfikujących:
int test;
*Postać nie jest częścią żadnego identyfikatora; jest to osobny token (interpunkcja). Więc jeśli piszesz
int*test;
kompilator widzi 4 oddzielne znaki - int, *, test, i ;. W związku z tym białe znaki nie są znaczące w deklaracjach wskaźników i we wszystkich
int *test;
int* test;
int*test;
int * test;
są interpretowane w ten sam sposób.
Drugim elementem układanki jest sposób działania deklaracji w C i C ++ 1 . Deklaracje są podzielone na dwie główne części - sekwencję specyfikatorów deklaracji ( specyfikatory klasy pamięci, specyfikatory typu, kwalifikatory typów itp.), Po których następuje rozdzielona przecinkami lista (prawdopodobnie zainicjowanych) deklaratorów . W deklaracji
unsigned long int a[10]={0}, *p=NULL, f(void);
specyfikatory deklaracji są unsigned long inti declarators są a[10]={0}, *p=NULLi f(void). Do wprowadza declarator nazwa rzeczy są zadeklarowane ( a, p, if ) wraz z informacjami o tablicowości, wskaźnikach i funkcjach tej rzeczy. Deklarator może również mieć skojarzony inicjator.
Typ ato „10-elementowa tablica unsigned long int”. Ten typ jest w pełni określony przez połączenie specyfikatorów deklaracji i deklaratora, a wartość początkowa jest określana za pomocą inicjatora ={0}. Podobnie, typ pjest „wskaźnikiem do unsigned long int” i ponownie ten typ jest określany przez kombinację specyfikatorów deklaracji i deklaratora, i jest inicjowany NULL. A typem fjest „funkcja powracająca unsigned long int” z tego samego powodu.
To jest klucz - nie ma specyfikatora typu „wskaźnik do” , tak jak nie ma specyfikatora typu „tablica”, tak jak nie ma specyfikatora typu „zwracającego funkcję”. Nie możemy zadeklarować tablicy jako
int[10] a;
ponieważ operand klasy [] operatora to anie int. Podobnie w deklaracji
int* p;
operand *jestp , nie int. Ale ponieważ operator pośredni jest jednoargumentowy, a białe znaki nie są znaczące, kompilator nie będzie narzekał, jeśli napiszemy go w ten sposób. Jednak zawsze jest interpretowane jako int (*p);.
Dlatego jeśli piszesz
int* p, q;
operand * jest p, więc zostanie zinterpretowany jako
int (*p), q;
Tak więc wszystkie
int *test1, test2;
int* test1, test2;
int * test1, test2;
zrób to samo - we wszystkich trzech przypadkach test1jest operandem *i dlatego ma typ „wskaźnika do”int ”, natomiast test2ma typ int.
Deklaratory mogą być dowolnie złożone. Możesz mieć tablice wskaźników:
T *a[N];
możesz mieć wskaźniki do tablic:
T (*a)[N];
możesz mieć funkcje zwracające wskaźniki:
T *f(void);
możesz mieć wskaźniki do funkcji:
T (*f)(void)
możesz mieć tablice wskaźników do funkcji:
T (*a[N])(void)
możesz mieć funkcje zwracające wskaźniki do tablic:
T (*f(void))[N];
możesz mieć funkcje zwracające wskaźniki do tablic wskaźników do funkcji zwracających wskaźniki do T:
T *(*(*f(void))[N])(void)
a następnie masz signal:
void (int)))(int);
co brzmi jak
signal -- signal
signal( ) -- is a function taking
signal( ) -- unnamed parameter
signal(int ) -- is an int
signal(int, ) -- unnamed parameter
signal(int, (*) ) -- is a pointer to
signal(int, (*)( )) -- a function taking
signal(int, (*)( )) -- unnamed parameter
signal(int, (*)(int)) -- is an int
signal(int, void (*)(int)) -- returning void
(*signal(int, void (*)(int))) -- returning a pointer to
(*signal(int, void (*)(int)))( ) -- a function taking
(*signal(int, void (*)(int)))( ) -- unnamed parameter
(*signal(int, void (*)(int)))(int) -- is an int
void (*signal(int, void (*)(int)))(int)
a to ledwo zarysowuje powierzchnię tego, co jest możliwe. Ale zwróć uwagę, że rodzaj tablicy, wskaźnik i funkcja są zawsze częścią deklaratora, a nie specyfikatora typu.
Jedna rzecz, na którą należy zwrócić uwagę - constmożna modyfikować zarówno typ wskaźnika, jak i typ wskazany:
const int *p;
int const *p;
Oba powyższe są deklarowane pjako wskaźnik do const intobiektu. Możesz napisać nową wartość, aby pustawić ją tak, aby wskazywała na inny obiekt:
const int x = 1;
const int y = 2;
const int *p = &x;
p = &y;
ale nie możesz pisać do wskazanego obiektu:
*p = 3;
Jednak,
int * const p;
deklaruje pjako constwskaźnik do wartości innej niż stała int; możesz napisać do rzeczy, na którą pwskazuje
int x = 1;
int y = 2;
int * const p = &x;
*p = 3;
ale nie możesz ustawić pwskazywania na inny obiekt:
p = &y
To prowadzi nas do trzeciego elementu układanki - dlaczego deklaracje są tak skonstruowane.
Celem jest, aby struktura deklaracji ściśle odzwierciedlała strukturę wyrażenia w kodzie („deklaracja naśladuje użycie”). Na przykład, załóżmy, że mamy tablicę wskaźników do intnamed api chcemy uzyskać dostęp do intwartości wskazywanej przez i-ty element. Dostęp do tej wartości uzyskalibyśmy w następujący sposób:
printf( "%d", *ap[i] );
Ekspresja *ap[i] jest typu int; w związku z tym deklaracja apjest zapisana jako
int *ap[N];
Deklarator *ap[N]ma taką samą strukturę jak wyrażenie *ap[i]. Operatory *i []zachowują się w taki sam sposób w deklaracji, jak w wyrażeniu - []mają wyższy priorytet niż jednoargumentowe *, więc operand *is ap[N](jest analizowany jako *(ap[N])).
Jako inny przykład załóżmy, że mamy wskaźnik do tablicy o intnazwie named pai chcemy uzyskać dostęp do wartości i'-tego elementu. Napisalibyśmy to jako
printf( "%d", (*pa)[i] )
Typ wyrażenia (*pa)[i]to int, więc deklaracja jest zapisywana jako
int (*pa)[N];
Ponownie, obowiązują te same zasady pierwszeństwa i kojarzenia. W tym przypadku nie chcemy wyłuskiwać i'-tego elementu pa, chcemy uzyskać dostęp do i' -tego elementu, na który pa wskazuje , więc musimy jawnie zgrupować* operator zpa .
Te *, []i ()operatorzy są częścią wyrażenia w kodzie, więc wszystkie one są częścią declarator w deklaracji. Deklarator mówi, jak używać obiektu w wyrażeniu. Jeśli masz taką deklarację int *p;, która mówi ci, że wyrażenie *pw twoim kodzie zwróci intwartość. Jako rozszerzenie mówi ci, że wyrażenie pzwraca wartość typu „wskaźnik do int”, lub int *.
A co z takimi rzeczami, jak obsada i sizeofwyrażenia, gdzie używamy rzeczy takich jak (int *)lub sizeof (int [10])lub takich rzeczy? Jak czytam coś takiego
void foo( int *, int (*)[10] );
Nie ma deklaratora, *czy []operatory i nie modyfikują typu bezpośrednio?
Cóż, nie - nadal istnieje deklarator, tylko z pustym identyfikatorem (znany jako abstrakcyjny deklarator ). Jeśli będziemy reprezentować pusty identyfikator z Î symbolu, to możemy czytać takie rzeczy jak (int *λ), sizeof (int λ[10])oraz
void foo( int *λ, int (*λ)[10] );
i zachowują się dokładnie tak, jak każda inna deklaracja. int *[10]reprezentuje tablicę 10 wskaźników, podczas gdy int (*)[10]reprezentuje wskaźnik do tablicy.
A teraz uparta część tej odpowiedzi. Nie podoba mi się konwencja C ++ deklarowania prostych wskaźników jako
T* p;
i uważaj to za złą praktykę z następujących powodów:
- Nie jest to zgodne ze składnią;
- Wprowadza zamieszanie (o czym świadczy to pytanie, wszystkie duplikaty tego pytania, pytania o znaczenie
T* p, q;, wszystkie duplikaty tych pytań itp.);
- To nie jest wewnętrznie spójne - deklarowanie tablicy wskaźników jako
T* a[N]asymetryczne w użyciu (chyba że masz zwyczaj pisania * a[i]);
- Nie można go zastosować do typów wskaźnika do tablicy lub wskaźnika do funkcji (chyba że utworzysz typedef tylko po to, abyś mógł
T* pczysto zastosować konwencję, która ... nie );
- Powód takiego postępowania - „podkreśla to, że obiekt jest wskazany” - jest fałszywy. Nie można go zastosować do tablic lub typów funkcji i myślę, że te cechy są równie ważne, aby je podkreślić.
W końcu oznacza to po prostu zdezorientowane myślenie o tym, jak działają systemy typów w dwóch językach.
Istnieją dobre powody, aby zgłaszać pozycje osobno; obejście złej praktyki ( T* p, q;) nie jest jednym z nich. Jeśli poprawnie napiszesz swoje deklaratory ( T *p, q;), jest mniej prawdopodobne, że wprowadzisz zamieszanie.
Uważam to za celowe pisanie wszystkich prostych forpętli jako
i = 0;
for( ; i < N; )
{
...
i++
}
Składniowo poprawne, ale mylące, a zamiar prawdopodobnie zostanie źle zinterpretowany. Jednak T* p;konwencja ta jest zakorzeniona w społeczności C ++ i używam jej we własnym kodzie C ++, ponieważ spójność w całym kodzie jest dobra, ale za każdym razem, gdy to robię, swędzi mnie.
- Będę używać terminologii C - terminologia C ++ jest trochę inna, ale koncepcje są w dużej mierze takie same.