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, inttest
po którym następuje znak interpunkcyjny ;
. Na int
tym 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 int
i declarators są a[10]={0}
, *p=NULL
i 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 a
to „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 p
jest „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 f
jest „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 a
nie 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 test1
jest operandem *
i dlatego ma typ „wskaźnika do”int
”, natomiast test2
ma 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ę - const
można modyfikować zarówno typ wskaźnika, jak i typ wskazany:
const int *p;
int const *p;
Oba powyższe są deklarowane p
jako wskaźnik do const int
obiektu. Możesz napisać nową wartość, aby p
ustawić 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 p
jako const
wskaźnik do wartości innej niż stała int
; możesz napisać do rzeczy, na którą p
wskazuje
int x = 1;
int y = 2;
int * const p = &x;
*p = 3;
ale nie możesz ustawić p
wskazywania 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 int
named ap
i chcemy uzyskać dostęp do int
wartoś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 ap
jest 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 int
nazwie named pa
i 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 *p
w twoim kodzie zwróci int
wartość. Jako rozszerzenie mówi ci, że wyrażenie p
zwraca wartość typu „wskaźnik do int
”, lub int *
.
A co z takimi rzeczami, jak obsada i sizeof
wyraż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* p
czysto 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 for
pę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.