Es gibt drei Teile dieses Puzzles.
Der erste Punkt ist, dass Leerzeichen in C und C++ normalerweise keine Bedeutung haben, außer dass sie benachbarte Token trennen, die ansonsten nicht voneinander zu unterscheiden sind.
In der Vorverarbeitungsphase wird der Ausgangstext in eine Folge von Token - Bezeichner, Interpunktionszeichen, numerische Literale, String-Literale, usw. Diese Folge von Token wird später auf Syntax und Bedeutung analysiert. Der Tokenizer ist "gierig" und wird das längste gültige Token bilden, das möglich ist. Wenn Sie etwas schreiben wie
inttest;
sieht der Tokenizer nur zwei Token - den Bezeichner inttest
gefolgt von dem Interpunktionszeichen ;
. Es erkennt nicht int
in diesem Stadium nicht als separates Schlüsselwort verwenden (dies geschieht später im Prozess). Damit die Zeile als Deklaration einer Ganzzahl mit dem Namen test
müssen wir Leerzeichen verwenden, um die Bezeichner-Token zu trennen:
int test;
Le site *
Zeichen ist nicht Teil eines Bezeichners; es ist ein separates Token (Interpunktionszeichen). Wenn Sie also schreiben
int*test;
sieht der Compiler 4 separate Token - int
, *
, test
y ;
. Daher sind Leerzeichen in Zeigerdeklarationen nicht von Bedeutung, und alle
int *test;
int* test;
int*test;
int * test;
werden auf die gleiche Weise interpretiert.
Das zweite Teil des Puzzles ist, wie Deklarationen in C und C++ tatsächlich funktionieren 1 . Erklärungen sind in zwei Hauptteile unterteilt - eine Folge von Deklarationsspezifizierer (Speicherklassenspezifizierer, Typenspezifizierer, Typqualifizierer usw.), gefolgt von einer durch Komma getrennten Liste von (möglicherweise initialisierten) Deklaratoren . In der Erklärung
unsigned long int a[10]={0}, *p=NULL, f(void);
Die Deklarationsspezifikationen sind unsigned long int
und die Deklaratoren sind a[10]={0}
, *p=NULL
y f(void)
. Der Deklarator leitet den Namen des zu deklarierenden Gegenstandes ein ( a
, p
y f
) zusammen mit Informationen über die Array-Eigenschaft, Zeiger-Eigenschaft und Funktions-Eigenschaft dieses Dings. Ein Deklarator kann auch einen zugehörigen Initialisierer haben.
Die Art der a
ist "10-Elemente-Array von unsigned long int
". Dieser Typ wird vollständig durch den Kombination der Deklarationsspezifizierer und des Deklarators, und der Anfangswert wird mit dem Initialisierer ={0}
. In ähnlicher Weise wird die Art der p
ist "Zeiger auf unsigned long int
", und auch dieser Typ wird durch die Kombination der Deklarationsangaben und des Deklarators spezifiziert und wird mit NULL
. Und die Art der f
ist "Funktion, die zurückkehrt unsigned long int
" mit der gleichen Argumentation.
Das ist der Schlüssel - es gibt keinen "Zeiger auf". Typ-Bezeichner Es gibt keinen "Array-of"-Typ, genauso wenig wie es einen "function-returning"-Typ gibt. Wir können ein Array nicht deklarieren als
int[10] a;
weil der Operand der Methode []
Betreiber ist a
, nicht int
. Ähnlich verhält es sich mit der Erklärung
int* p;
der Operand von *
es p
, nicht int
. Da der Indirektionsoperator jedoch unär ist und Leerzeichen keine Rolle spielen, wird der Compiler sich nicht beschweren, wenn wir ihn so schreiben. Allerdings ist es immer interpretiert als int (*p);
.
Wenn Sie also schreiben
int* p, q;
der Operand von *
es p
und wird daher wie folgt interpretiert
int (*p), q;
Somit sind alle
int *test1, test2;
int* test1, test2;
int * test1, test2;
das Gleiche tun - in allen drei Fällen, test1
ist der Operand von *
und hat somit den Typ "Zeiger auf int
", während test2
hat Typ int
.
Deklaratoren können beliebig komplex werden. Sie können Arrays von Zeigern haben:
T *a[N];
können Sie Zeiger auf Arrays haben:
T (*a)[N];
können Sie Funktionen haben, die Zeiger zurückgeben:
T *f(void);
können Sie Zeiger auf Funktionen haben:
T (*f)(void);
können Sie Arrays von Zeigern auf Funktionen haben:
T (*a[N])(void);
können Sie Funktionen haben, die Zeiger auf Arrays zurückgeben:
T (*f(void))[N];
können Sie Funktionen haben, die Zeiger auf Arrays von Zeigern auf Funktionen zurückgeben, die Zeiger auf T
:
T *(*(*f(void))[N])(void); // yes, it's eye-stabby. Welcome to C and C++.
und dann haben Sie signal
:
void (*signal(int, void (*)(int)))(int);
die wie folgt lautet
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
und das kratzt gerade mal an der Oberfläche dessen, was möglich ist. Beachten Sie jedoch, dass Array-, Zeiger- und Funktionseigenschaften immer Teil des Deklarators sind, nicht des Typbezeichners.
Eine Sache, auf die Sie achten sollten - const
kann sowohl den Zeigertyp als auch den Typ, auf den gezeigt wird, ändern:
const int *p;
int const *p;
Die beiden oben genannten erklären p
als Zeiger auf eine const int
Objekt. Sie können einen neuen Wert in p
auf ein anderes Objekt verweisen:
const int x = 1;
const int y = 2;
const int *p = &x;
p = &y;
aber Sie können nicht in das Objekt, auf das gezeigt wird, schreiben:
*p = 3; // constraint violation, the pointed-to object is const
Allerdings,
int * const p;
erklärt p
als const
Zeiger auf eine Nicht-Konst int
; Sie können die Sache anschreiben p
zeigt auf
int x = 1;
int y = 2;
int * const p = &x;
*p = 3;
aber Sie können nicht die p
um auf ein anderes Objekt zu verweisen:
p = &y; // constraint violation, p is const
Das bringt uns zum dritten Teil des Puzzles - warum Erklärungen auf diese Weise strukturiert sind.
Die Absicht ist, dass die Struktur einer Deklaration die Struktur eines Ausdrucks im Code genau widerspiegeln soll ("declaration mimics use"). Nehmen wir zum Beispiel an, wir haben ein Array mit Zeigern auf int
namens ap
und wir wollen auf die int
Wert, auf den der i
Element. Wir würden auf diesen Wert wie folgt zugreifen:
printf( "%d", *ap[i] );
Le site Ausdruck *ap[i]
hat Typ int
Die Erklärung von ap
wird geschrieben als
int *ap[N]; // ap is an array of pointer to int, fully specified by the combination
// of the type specifier and declarator
Der Deklarator *ap[N]
hat die gleiche Struktur wie der Ausdruck *ap[i]
. Die Betreiber *
y []
verhalten sich in einer Deklaration genauso wie in einem Ausdruck - []
hat einen höheren Vorrang als unäre *
, so dass der Operand von *
es ap[N]
(es wird geparst als *(ap[N])
).
Ein weiteres Beispiel: Nehmen wir an, wir haben einen Zeiger auf ein Array von int
namens pa
und wir wollen auf den Wert des Feldes i
Element. Wir würden das schreiben als
printf( "%d", (*pa)[i] );
Der Typ des Ausdrucks (*pa)[i]
es int
so dass die Erklärung wie folgt geschrieben wird
int (*pa)[N];
Auch hier gelten die gleichen Regeln für Vorrang und Assoziativität. In diesem Fall wollen wir die Dereferenzierung der i
Element von pa
wollen wir auf die i
Das Element von was pa
zeigt auf gruppieren, also müssen wir explizit die *
Betreiber mit pa
.
Le site *
, []
y ()
Operatoren sind alle Teil des Ausdruck im Code, sie sind also alle Teil der Deklarator in der Erklärung. Der Deklarator sagt Ihnen, wie Sie das Objekt in einem Ausdruck verwenden können. Wenn Sie eine Deklaration haben wie int *p;
die Ihnen sagt, dass der Ausdruck *p
in Ihrem Code wird eine int
Wert. Im weiteren Sinne sagt es Ihnen, dass der Ausdruck p
ergibt einen Wert vom Typ "Zeiger auf int
", oder int *
.
Und was ist mit Dingen wie Guss und sizeof
Ausdrücke, in denen wir Dinge verwenden wie (int *)
o sizeof (int [10])
oder solche Dinge? Wie lese ich etwas wie
void foo( int *, int (*)[10] );
Es gibt keinen Deklarator, sind nicht die *
y []
Operatoren, die den Typ direkt ändern?
Nun, nein - es gibt immer noch einen Deklarator, nur mit einem leeren Bezeichner (bekannt als abstrakter Deklarator ). Wenn wir einen leeren Bezeichner mit dem Symbol darstellen, dann können wir diese Dinge lesen als (int *)
, sizeof (int [10])
und
void foo( int *λ, int (*λ)[10] );
und sie verhalten sich genau wie jede andere Erklärung. int *[10]
ein Array von 10 Zeigern darstellt, während int (*)[10]
stellt einen Zeiger auf ein Array dar.
Und nun der rechthaberische Teil dieser Antwort. Ich bin kein Freund der C++-Konvention, einfache Zeiger zu deklarieren als
T* p;
und betrachten sie schlechte Praxis aus den folgenden Gründen:
- Das ist nicht mit der Syntax vereinbar;
- Sie stiftet Verwirrung (wie diese Frage, alle Duplikate zu dieser Frage, Fragen zur Bedeutung von
T* p, q;
alle Duplikate nach die Fragen, usw.);
- Es ist intern nicht konsistent - die Deklaration eines Arrays von Zeigern als
T* a[N]
ist bei der Verwendung asymmetrisch (es sei denn, Sie haben die Angewohnheit, die * a[i]
);
- Sie kann nicht auf Zeiger-auf-Array- oder Zeiger-auf-Funktions-Typen angewandt werden (es sei denn, Sie erstellen ein Typedef, nur damit Sie die
T* p
saubere Konvention, die... keine );
- Der Grund dafür - "es betont die Zeigerhaftigkeit des Objekts" - ist fadenscheinig. Sie kann nicht auf Array- oder Funktionstypen angewandt werden, und ich würde denken, dass diese Eigenschaften genauso wichtig zu betonen sind.
Letzten Endes deutet dies nur auf ein verwirrtes Denken darüber hin, wie die Typensysteme der beiden Sprachen funktionieren.
Es gibt gute Gründe, Posten getrennt zu deklarieren; die Umgehung einer schlechten Praxis ( T* p, q;
) gehört nicht dazu. Wenn Sie Ihre Deklaratoren schreiben richtig ( T *p, q;
), ist die Wahrscheinlichkeit geringer, dass Sie Verwirrung stiften.
Ich betrachte es so, als würden Sie absichtlich alle Ihre einfachen for
Schleifen als
i = 0;
for( ; i < N; )
{
...
i++;
}
Syntaktisch gültig, aber verwirrend, und es besteht die Gefahr, dass die Absicht falsch interpretiert wird. Allerdings ist die T* p;
Die Konvention ist in der C++-Gemeinschaft fest verankert, und ich verwende sie in meinem eigenen C++-Code, weil Konsistenz in der gesamten Codebasis eine gute Sache ist, aber es juckt mich jedes Mal, wenn ich es tue.
- Ich werde die C-Terminologie verwenden - die C++-Terminologie ist ein wenig anders, aber die Konzepte sind weitgehend dieselben.
19 Stimmen
In C/C++ ändern Leerzeichen die Bedeutung nicht.
38 Stimmen
7.
int*test;
?4 Stimmen
+1, denn ich hatte nur daran gedacht, nach 1 - 3 zu fragen. Beim Lesen dieser Frage habe ich etwas über 4 - 6 gelernt, woran ich nie gedacht hatte.
0 Stimmen
@Sulthan Das stimmt in 99 % der Fälle, aber nicht immer. Von der Spitze von meinem Kopf gab es die Art der schablonenartigen Typ in schablonenartigen Typ Raum Anforderung (vor C++11). In
Foo<Bar<char>>
die>>
musste geschrieben werden> >
um nicht als Rechtsverschiebung behandelt zu werden.4 Stimmen
@AnorZaken Sie haben recht, das ist ein ziemlich alter Kommentar. Es gibt mehrere Situationen, in denen ein Leerzeichen seine Bedeutung ändert, zum Beispiel das Inkrement
++
Operator nicht durch ein Leerzeichen getrennt werden kann, können Bezeichner nicht durch ein Leerzeichen getrennt werden (und das Ergebnis kann für den Compiler immer noch legal sein, aber mit undefiniertem Laufzeitverhalten). Die genauen Situationen sind sehr schwer zu definieren, wenn man bedenkt, was für ein Syntaxchaos C/C++ ist.0 Stimmen
@Sulthan ja, diese Fälle, die Sie jetzt erwähnen, sollten ziemlich offensichtlich sein, ich meinte, dass es ein paar nicht offensichtliche Fälle gibt, wo ein Leerzeichen ist erforderlich . Wie auch immer, ich wollte diese Anmerkung nur zu Protokoll geben, sie war nicht als Kritik an Ihrem Kommentar gedacht.
1 Stimmen
@JinKwon: Whitespace ist nur notwendig, um Token zu trennen, die sonst nicht unterschieden werden können. Da
*
nicht Teil eines Bezeichners ist (es ist ein eigenständiges Token), ist kein Leerzeichen zur Abtrennung vonint
,*
ytest
. Er wird immer noch geparst alsint (*test)
.5 Stimmen
Ich verstehe nicht, warum immer wieder behauptet wird, dies sei "nur Ästhetik" oder "Stil" oder "eine Frage der Meinung". Die Tatsache, dass
int* test,test2;
nicht das tut, was man erwarten würde, bedeutet, dass es falsch ist, ein Ergebnis eines Missverständnisses der Sprache, und dassint *test,test2;
ist richtig.1 Stimmen
...und dass
int* test; int test2;
ist richtig.0 Stimmen
Ich denke, das war ein weiterer Grund, warum intelligente Zeiger entwickelt wurden. Um das Sternchen überhaupt zu vermeiden.