Niedawno zdecydowałem, że muszę w końcu nauczyć się C / C ++ i jest jedna rzecz, której nie rozumiem, jeśli chodzi o wskaźniki, a dokładniej ich definicję.
A co z tymi przykładami:
int* test;
int *test;
int * test;
int* test,test2;
int *test,test2;
int * test,test2;
Otóż, według mojego zrozumienia, pierwsze trzy przypadki robią to samo: Test nie jest intem, ale wskaźnikiem do jednego.
Drugi zestaw przykładów jest nieco bardziej skomplikowany. W przypadku 4 zarówno test, jak i test2 będą wskaźnikami do int, podczas gdy w przypadku 5 tylko test jest wskaźnikiem, podczas gdy test2 jest „prawdziwą” liczbą int. A co z przypadkiem 6? Tak samo jak w przypadku 5?
c++
c
pointers
declaration
Michael Stum
źródło
źródło
int*test;
?Foo<Bar<char>>
>>
> >
++
operator inkrementacji nie może zostać podzielony spacją, identyfikatorów nie można podzielić spacją (a wynik może być nadal prawidłowy dla kompilatora, ale z niezdefiniowanym zachowaniem w czasie wykonywania). Dokładne sytuacje są bardzo trudne do zdefiniowania, biorąc pod uwagę bałagan składniowy, jakim jest C / C ++.Odpowiedzi:
4, 5 i 6 to to samo, tylko test to wskaźnik. Jeśli potrzebujesz dwóch wskazówek, powinieneś użyć:
int *test, *test2;
Lub jeszcze lepiej (żeby wszystko było jasne):
int* test; int* test2;
źródło
Puste miejsca wokół gwiazdek nie mają znaczenia. Wszystkie trzy oznaczają to samo:
int* test; int *test; int * test;
„
int *var1, var2
” To zła składnia, która ma po prostu zmylić ludzi i należy jej unikać. Rozszerza się do:int *var1; int var2;
źródło
int*test
.int *test
( google-styleguide.googlecode.com/svn/trunk/ ... ). Po prostu bądź konsekwentnyWiele wytycznych dotyczących kodowania zaleca deklarowanie tylko jednej zmiennej w wierszu . Pozwala to uniknąć nieporozumień, jakie miałeś przed zadaniem tego pytania. Większość programistów C ++, z którymi pracowałem, wydaje się trzymać się tego.
Na marginesie wiem, ale coś, co uznałem za przydatne, to czytanie deklaracji wstecz.
int* test; // test is a pointer to an int
Zaczyna to działać bardzo dobrze, zwłaszcza gdy zaczynasz deklarować wskaźniki const i trudno jest dowiedzieć się, czy to wskaźnik jest stała, czy też to, na co wskazuje wskaźnik, jest stała.
int* const test; // test is a const pointer to an int int const * test; // test is a pointer to a const int ... but many people write this as const int * test; // test is a pointer to an int that's const
źródło
Użyj „Reguły spirali zgodnej z ruchem wskazówek zegara”, aby pomóc w analizowaniu deklaracji C / C ++;
Ponadto deklaracje powinny być w miarę możliwości w oddzielnych oświadczeniach (co jest prawdą w większości przypadków).
źródło
int* x;
raczej do stylu niż doint *x;
stylu tradycyjnego . Oczywiście odstępy nie mają znaczenia dla kompilatora, ale mają wpływ na ludzi. Odmowa właściwej składni prowadzi do reguł stylu, które mogą denerwować i wprawiać czytelników w zakłopotanie.Jak wspomnieli inni, 4, 5 i 6 są takie same. Często ludzie używają tych przykładów, aby argument, że
*
należy do zmiennej, a nie do typu. Chociaż jest to kwestia stylu, toczy się dyskusja, czy należy o tym myśleć i pisać w ten sposób:int* x; // "x is a pointer to int"
lub w ten sposób:
int *x; // "*x is an int"
FWIW Jestem w pierwszym obozie, ale inni argumentują za drugą formą, ponieważ (w większości) rozwiązuje ona ten konkretny problem:
int* x,y; // "x is a pointer to int, y is an int"
który może wprowadzać w błąd; zamiast tego napisałbyś albo
int *x,y; // it's a little clearer what is going on here
lub jeśli naprawdę potrzebujesz dwóch wskazówek,
int *x, *y; // two pointers
Osobiście mówię, że trzymaj to jedną zmienną w wierszu, wtedy nie ma znaczenia, jaki styl preferujesz.
źródło
int *MyFunc(void)
? a*MyFunc
jest funkcją zwracającąint
? Nie. Oczywiście powinniśmy napisaćint* MyFunc(void)
i powiedzieć, żeMyFunc
jest to funkcja zwracającaint*
. Dla mnie jest to jasne, reguły analizowania gramatyki C i C ++ są po prostu nieprawidłowe dla deklaracji zmiennych. powinny zawierać kwalifikację wskaźnika jako część typu wspólnego dla całej sekwencji przecinków.*MyFunc()
jestint
. Problem ze składnią C polega na mieszaniu składni przedrostka i postfiksa - gdybyśmy użyli tylko przyrostka, nie byłoby zamieszania.int const* x;
, które uważam za równie mylące jaka * x+b * y
.#include <type_traits> std::add_pointer<int>::type test, test2;
źródło
#include <windows.h>LPINT test, test2;
W punktach 4, 5 i 6
test
jest zawsze wskaźnikiem itest2
nie jest wskaźnikiem. Białe znaki (prawie) nigdy nie mają znaczenia w C ++.źródło
Uzasadnienie w C jest takie, że deklarujesz zmienne w sposób, w jaki ich używasz. Na przykład
char *a[100];
mówi, że
*a[42]
będziechar
. Ia[42]
wskaźnik postaci. A zatema
jest tablicą wskaźników znaków.Dzieje się tak, ponieważ pierwotni autorzy kompilatorów chcieli używać tego samego parsera do wyrażeń i deklaracji. (Niezbyt rozsądny powód do wyboru projektu językowego)
źródło
char* a[100];
również wnioskuje, że*a[42];
będzie tochar
ia[42];
wskaźnik znaku.a[42]
jestchar
wskaźnikiem i*a[42]
jest znakiem.Moim zdaniem odpowiedź brzmi OBIE, w zależności od sytuacji. Ogólnie rzecz biorąc, IMO, lepiej jest umieścić gwiazdkę obok nazwy wskaźnika, a nie typu. Porównaj np .:
int *pointer1, *pointer2; // Fully consistent, two pointers int* pointer1, pointer2; // Inconsistent -- because only the first one is a pointer, the second one is an int variable // The second case is unexpected, and thus prone to errors
Dlaczego drugi przypadek jest niespójny? Bo np.
int x,y;
Deklaruje dwie zmienne tego samego typu, ale typ jest wymieniany tylko raz w deklaracji. Stwarza to precedens i oczekiwane zachowanie. Iint* pointer1, pointer2;
jest z tym niespójny, ponieważ deklarujepointer1
jako wskaźnik, alepointer2
jest zmienną całkowitą. Są wyraźnie podatne na błędy, dlatego należy ich unikać (umieszczając gwiazdkę obok nazwy wskaźnika, a nie typu).Jednak istnieją pewne wyjątki , gdzie może nie być w stanie umieścić gwiazdkę obok nazwy obiektu (a tam gdzie ma to znaczenie, gdzie można umieścić go) bez uzyskania niepożądanego rezultatu - na przykład:
MyClass *volatile MyObjName
void test (const char *const p) // const value pointed to by a const pointer
Wreszcie, w niektórych przypadkach może być prawdopodobnie bardziej zrozumiałe umieszczenie gwiazdki obok nazwy typu , np .:
void* ClassName::getItemPtr () {return &item;} // Clear at first sight
źródło
Powiedziałbym, że początkową konwencją było umieszczenie gwiazdy po stronie nazwy wskaźnika (po prawej stronie deklaracji
w języku programowania c autorstwa Dennisa M. Ritchiego gwiazdy znajdują się po prawej stronie deklaracji.
patrząc na kod źródłowy Linuksa pod adresem https://github.com/torvalds/linux/blob/master/init/main.c widać, że gwiazdka jest również po prawej stronie.
Możesz przestrzegać tych samych zasad, ale nie jest to wielka sprawa, jeśli umieścisz gwiazdki po stronie czcionki. Pamiętaj, że spójność jest ważna, więc zawsze, ale gwiazda po tej samej stronie, niezależnie od tego, którą stronę wybierzesz.
źródło
Wskaźnik jest modyfikatorem typu. Najlepiej czytać je od prawej do lewej, aby lepiej zrozumieć, jak gwiazdka modyfikuje tekst. „int *” można odczytać jako „wskaźnik do int”. W wielu deklaracjach należy określić, że każda zmienna jest wskaźnikiem, w przeciwnym razie zostanie utworzona jako zmienna standardowa.
1,2 i 3) Test jest typu (int *). Białe znaki nie mają znaczenia.
4,5 i 6) Test jest typu (int *). Test2 jest typu int. Znów białe znaki są nieistotne.
źródło
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
tokenizer widzi tylko dwa tokeny - identyfikator,
inttest
po którym następuje znak interpunkcyjny;
. Naint
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 nazwietest
, 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 piszeszint*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 wszystkichint *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
if(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 tablicaunsigned 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, typp
jest „wskaźnikiem dounsigned long int
” i ponownie ten typ jest określany przez kombinację specyfikatorów deklaracji i deklaratora, i jest inicjowanyNULL
. A typemf
jest „funkcja powracającaunsigned 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 toa
nieint
. Podobnie w deklaracjiint* p;
operand
*
jestp
, nieint
. 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 jakoint (*p);
.Dlatego jeśli piszesz
int* p, q;
operand
*
jestp
, więc zostanie zinterpretowany jakoint (*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
”, natomiasttest2
ma typint
.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); // yes, it's eye-stabby. Welcome to C and C++.
a następnie masz
signal
:void (*signal(int, 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); -- returning void
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 doconst int
obiektu. Możesz napisać nową wartość, abyp
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; // constraint violation, the pointed-to object is const
Jednak,
int * const p;
deklaruje
p
jakoconst
wskaźnik do wartości innej niż stałaint
; możesz napisać do rzeczy, na którąp
wskazujeint x = 1; int y = 2; int * const p = &x; *p = 3;
ale nie możesz ustawić
p
wskazywania na inny obiekt:p = &y; // constraint violation, p is const
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
namedap
i chcemy uzyskać dostęp doint
wartości wskazywanej przezi
-ty element. Dostęp do tej wartości uzyskalibyśmy w następujący sposób:printf( "%d", *ap[i] );
Ekspresja
*ap[i]
jest typuint
; w związku z tym deklaracjaap
jest zapisana jakoint *ap[N]; // ap is an array of pointer to int, fully specified by the combination // of the type specifier and declarator
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*
isap[N]
(jest analizowany jako*(ap[N])
).Jako inny przykład załóżmy, że mamy wskaźnik do tablicy o
int
nazwie namedpa
i chcemy uzyskać dostęp do wartościi
'-tego elementu. Napisalibyśmy to jakoprintf( "%d", (*pa)[i] );
Typ wyrażenia
(*pa)[i]
toint
, więc deklaracja jest zapisywana jakoint (*pa)[N];
Ponownie, obowiązują te same zasady pierwszeństwa i kojarzenia. W tym przypadku nie chcemy wyłuskiwać
i
'-tego elementupa
, chcemy uzyskać dostęp doi
' -tego elementu, na którypa
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óciint
wartość. Jako rozszerzenie mówi ci, że wyrażeniep
zwraca wartość typu „wskaźnik doint
”, lubint *
.A co z takimi rzeczami, jak obsada i
sizeof
wyrażenia, gdzie używamy rzeczy takich jak(int *)
lubsizeof (int [10])
lub takich rzeczy? Jak czytam coś takiegovoid 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])
orazvoid foo( int *λ, int (*λ)[10] );
i zachowują się dokładnie tak, jak każda inna deklaracja.
int *[10]
reprezentuje tablicę 10 wskaźników, podczas gdyint (*)[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:
T* p, q;
, wszystkie duplikaty tych pytań itp.);T* a[N]
asymetryczne w użyciu (chyba że masz zwyczaj pisania* a[i]
);T* p
czysto zastosować konwencję, która ... nie );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 jakoi = 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.źródło
Dobra zasada praktyczna: wiele osób wydaje się rozumieć te pojęcia przez: W C ++ wiele znaczeń semantycznych jest wyprowadzanych z lewego wiązania słów kluczowych lub identyfikatorów.
Weź na przykład:
int const bla;
Stała dotyczy słowa „int”. To samo dotyczy gwiazdek wskaźników, mają one zastosowanie do słowa kluczowego po lewej stronie. A rzeczywista nazwa zmiennej? Tak, to zostało zadeklarowane przez to, co z niego zostało.
źródło