Ist * in C ein Operator oder Teil eines Typs in einer Deklaration?

9

In C *wird der Indirektionsoperator oder der Dereferenzierungsoperator genannt. Ich verstehe, wie es funktioniert, wenn es in einer Anweisung verwendet wird. Es ist sinnvoll zu schreiben *poder * p, wenn man bedenkt, dass es sich um einen unären Operator handelt.

Manchmal wird jedoch in einer Deklaration a *verwendet.

void move(int *units) { ... }

oder

int *p = &x;

Es scheint mir seltsam, dass hier ein Operator verwendet wird. Sie können Dinge wie nicht tun

void move(int units++) { ... }

wo ++ist auch ein unärer Operator. Ist dies *auch der Indirektionsoperator oder hat dies *eine andere Bedeutung, wenn er etwas über den Typ des Zeigers aussagt? (Wenn dies der Fall ist, erwäge ich, dies beispielsweise int* unitsin Erklärungen und int x = *p;aus Gründen der Klarheit zu verwenden.)

In dieser Antwort heißt es:

Der C-Standard definiert für den Operator * nur zwei Bedeutungen:

  • Indirektionsoperator
  • Multiplikationsoperator

Ich habe auch Leute gesehen, die behaupteten, dass das *tatsächlich Teil des Typs ist. Das verwirrt mich.

Torm
quelle
1
In void move(int *units) { ... }ist es ein Indirektionsoperator. Als Teil des Typs betrachtet, kann es auch geschrieben werden void move(int* units) { ... }, obwohl ich den früheren Stil bevorzuge. Sie lesen beide als "int Zeiger". Siehe auch stackoverflow.com/a/8911253
Robert Harvey

Antworten:

16

Die kleinsten Teile der Sprache C sind lexikalische Token , wie Schlüsselwörter (zB int, if, break), Bezeichner (zB move, units) und andere.

*ist ein lexikalisches Element, das als Interpunktionszeichen bezeichnet wird (wie zum Beispiel {}()+). Ein Interpunktionszeichen kann je nach Verwendung unterschiedliche Bedeutungen haben:

Ein Interpunktionszeichen ist ein Symbol mit unabhängiger syntaktischer und semantischer Bedeutung. Je nach Kontext kann eine auszuführende Operation angegeben werden (...). In diesem Fall wird sie als Operator bezeichnet

Im C11-Standard wird Kapitel 6.5 über Ausdrücke *als unärer Operator (Abschnitt 6.5.3.2 für die Indirektion) und als multiplikativer Operator (Abschnitt 6.5.5) definiert, genau wie Sie es beschrieben haben. Wird *aber nur als Operator gemäß den grammatikalischen Regeln interpretiert, die gültige Ausdrücke bilden.

Es gibt aber auch ein Kapitel 6.2 über Konzepte, in dem erklärt wird, dass Zeiger auf einen Typ abgeleitete Typen sind. Der Abschnitt 6.7.6.1 über Zeigerdeklaratoren ist genauer:

Wenn in der Deklaration '' T D1 '' D1 die Form
* type-qualifier-list-opt D hat
und der in der Deklaration '' T D '' für ident angegebene Typ '' abgeleitet-deklarator-type-list ist T '', dann ist der für ident angegebene Typ '' abgeleiteter Deklarator-Typ-Liste Typ-Qualifizierer-Liste Zeiger auf T ''.

Dies macht in der Tat *Teil eines abgeleiteten Typs ( *bedeutet dort "Zeiger", aber es braucht immer einen anderen Typ, um zu sagen, auf welche Art von Dingen es auf etwas zeigt).

Bemerkung: Darin besteht kein Widerspruch. Sie verwenden das lexikalische Sprachelement jeden Tag so differenziert, wenn Sie Englisch sprechen: "ein Handle " bezeichnet etwas und " handhaben " bedeutet etwas zu tun, zwei völlig unterschiedliche grammatikalische Verwendungen desselben lexikalischen Elements.

Christophe
quelle
Das bedeutet, dass diese Interpretation nur eine gebräuchliche Sichtweise ist (aus der anderen Antwort): int * a Diese Anweisung könnte folgendermaßen interpretiert werden: Deklarieren Sie eine Variable mit dem Namen a, die bei der Dereferenzierung vom Typ int ist. -Wenn in der Tat das * eine unabhängige Bedeutung in einer Erklärung hat
Torm
@ Sturm genau!
Christophe
7

In C wird die Indirektion in einer Deklaration besser als Teil der Variablen als als Teil des Typs gelesen. Wenn ich habe:

int *a;

Diese Anweisung könnte folgendermaßen interpretiert werden: Deklarieren Sie eine Variable mit dem Namen a, die bei der Dereferenzierung vom Typ ist int.

Dies ist wichtig, wenn mehrere Variablen gleichzeitig deklariert werden:

int *a, b;   (1)
int* a, b;   (2)

Beide Deklarationen sind äquivalent und in beiden aund bnicht vom selben Typ - aist ein Zeiger auf ein int und bein int. Die Deklaration (2) sieht jedoch so aus, als ob der "Zeigertyp" Teil des Typs ist, wenn es sich tatsächlich um die Deklaration von handelt *a.

Erik
quelle
Das ist ein sehr guter Punkt, um dies anzusprechen, da es oft eine Verwechslung zwischen 1 und 2 gibt. Der Typ von a ist jedoch "Zeiger auf int". Selbst wenn das * nur auf a angewendet wird, ist es definitiv Teil des Typs . Sie könnten es beispielsweise in einem typedef verwenden (was Sie für einen Initialisierer nicht tun könnten).
Christophe
Ich war auf der Suche nach einem guten Argument für die Entscheidung zwischen den beiden Erklärungen. Ich deklariere Variable zwar selten als Liste, aber dennoch werde ich von nun an die erste Form in allen Fällen als am klarsten annehmen. Vielen Dank !
Newtopian
4

So fügen Sie den anderen richtigen Antworten hinzu:

Es ist nur ein Deklarationsausdruck, der mögliche Verwendungsausdrücke widerspiegelt. IIRC, ich denke, Kernigan oder Ritchie haben es ein Experiment genannt! Hier ist ein geringfügig unterstützender Text:

In den 70er Jahren, als Kernighan und Ritchie The C Programming Language schrieben, waren sie ziemlich ehrlich darüber, wie Erklärungen für das ungeübte Auge schnell unlesbar werden können:

C wird manchmal für die Syntax seiner Deklarationen geißelt, insbesondere für solche, die Zeiger auf Funktionen enthalten. Die Syntax ist ein Versuch , die Deklaration und die Verwendung in Einklang zu bringen . es funktioniert gut für die einfachen Fälle, aber es kann für die schwierigeren verwirrend sein, weil Deklarationen nicht von links nach rechts gelesen werden können und weil Klammern überbeansprucht werden. [...]

(Meine Betonung.)

http://codinghighway.com/2013/12/29/the-absolute-definitive-guide-to-decipher-c-declarations/

Also, ja, Sie haben Recht, dies sind in der Tat dieselben Operatoren, die auf Deklarationsausdrücke angewendet werden, obwohl, wie Sie beobachtet haben, nur einige der Operatoren sinnvoll sind (z. B. ++ nicht).

Ein solcher Stern ist in der Tat Teil des Typs der einzelnen deklarierten Variablen; Wie andere Poster angemerkt haben, kann es jedoch nicht (genauer gesagt nicht) auf andere Variablen übertragen werden, die gleichzeitig deklariert werden (dh in derselben Deklarationsanweisung) - jede Variable beginnt mit demselben Basistyp wie das (eventuelle) Ziel und erhält dann seine eigenen (Möglichkeit anzuwenden oder nicht) Zeiger-, Array- und Funktionsoperatoren, um seinen Typ zu vervollständigen.


Aus diesem Grund bevorzugen erfahrene Programmierer (1) int *p;gegenüber int* p;(2) und deklarieren nur eine einzige Variable pro Deklaration, obwohl die Sprache mehr zulässt.

Hinzu int (*p);kommt eine rechtliche Erklärung, dass diese Eltern einfach gruppiert sind und in diesem einfachen Fall nichts anderes tun als int *p. Dies ist dasselbe wie zu sagen, dass ein (nicht deklarativer) Ausdruck (i) >= (0)dasselbe ist wie i >= 0, wie Sie immer legal hinzufügen können ().

Ich erwähnen , dass als ein weiteres Beispiel, obwohl, warum Programmierer eine Form über die andere bevorzugen, so betrachten int (*p);vs int(* p);. Vielleicht können Sie sehen, wie die Klammern, der Stern usw. auf die Variable und nicht auf den Basistyp zutreffen (dh indem Sie alle zulässigen Klammern hinzufügen).

Erik Eidt
quelle
1

In dieser Funktionsdeklaration:

void move(int *units);

das *ist ein Teil des Typs, das heißt int*. Es ist kein Operator. Deshalb würden viele Leute es schreiben als:

void move(int* units);
BЈовић
quelle
1

Nur die *, []und ()Betreiber haben keine Bedeutung in Erklärungen (C ++ fügt hinzu &, aber wir gehen nicht in das hier).

In der Erklärung

int *p;       

Die int-ness von pwird vom Typbezeichner angegeben int, während die Zeigerzahl von pvom Deklarator angegeben wird *p.

Der Typ von pist "Zeiger auf int"; Dieser Typ wird vollständig durch die Kombination des Typspezifizierers intund des Deklarators angegeben *p.

In einer Deklaration führt der Deklarator den Namen der deklarierten Sache ( p) zusammen mit zusätzlichen Typinformationen ein, die nicht vom Typspezifizierer angegeben werden ("Zeiger auf"):

T v;     // v is a single object of type T, for any type T
T *p;    // p is a pointer to T, for any type T
T a[N];  // a is an N-element array of T, for any type T
T f();   // f is a function returning T, for any type T

Dies ist wichtig - Zeiger, Array und Funktion werden als Teil des Deklarators angegeben, nicht als Typspezifizierer 1 . Wenn du schreibst

int* a, b, c;

es wird analysiert als

int (*a), b, c;

so awird nur als Zeiger auf deklariert int; bund cwerden als reguläre ints deklariert .

Die *, []und ()Betreiber können kombiniert werden , um beliebig komplexe Typen zu erstellen:

T *a[N];      // a is an N-element array of pointers to T
T (*a)[N];    // a is a pointer to an N-element array of T
T *(*f[N])(); // f is an N-element array of pointers to functions 
              // returning pointer to T

T *(*(*(*f)[N])())[M]  // f is a pointer to an N-element array of pointers
                       // to functions returning pointers to M-element
                       // arrays of pointers to T

Beachten Sie, dass *, []und ()befolgen Sie in Deklarationen dieselben Vorrangregeln wie in Ausdrücken. *a[N]wird wie *(a[N])in Deklarationen und Ausdrücken analysiert .

Das wirklich Wichtige dabei ist, dass die Form einer Deklaration mit der Form des Ausdrucks im Code übereinstimmt. Zurück zu unserem ursprünglichen Beispiel haben wir einen Zeiger auf eine Ganzzahl namens p. Wenn wir diesen ganzzahligen Wert abrufen möchten, verwenden wir den *Operator zum Dereferenzieren pwie folgt :

x = *p;

Der Typ des Ausdrucks *p ist int, der sich aus der Deklaration ergibt

int *p;

Wenn wir ein Array von Zeigern auf haben doubleund einen bestimmten Wert abrufen möchten, indizieren wir das Array und dereferenzieren das Ergebnis:

y = *ap[i];

Auch die Art des Ausdrucks *ap[i] ist double, der aus der Erklärung folgt

double *ap[N];

Warum also nicht ++eine Rolle spielen , in einer Erklärung , wie *, []oder ()? Oder irgendein anderer Betreiber wie +oder ->oder &&?

Nun, im Grunde, weil die Sprachdefinition dies sagt. Er setzt nur beiseite *, []und ()jede Rolle in einer Erklärung zu spielen, da Sie angeben , Zeiger, Array in der Lage sein müssen, und Funktionstypen. Es gibt keinen separaten "Inkrement-this" -Typ, sodass Sie nicht ++Teil einer Deklaration sein müssen. Es gibt keine „bitweise diese“ Art, so dass keine Notwendigkeit für einstellige &, |, ^, oder ~entweder Teil einer Erklärung zu sein. Für Typen, die den .Elementauswahloperator verwenden, verwenden wir die Tags structund unionin der Deklaration. Für Typen, die den ->Operator verwenden, verwenden wir die Tags structund unionin Verbindung mit dem *Operator im Deklarator.


  1. Natürlich können Sie typedef Namen für Zeiger-, Array- und Funktionstypen erstellen, wie z
    typedef int *iptr;
    iptr a,b,c; // all three of a, b, and c are pointers to int
    
    Aber auch hier ist es der Deklarator *iptr , der den Zeiger des Typedef-Namens angibt.

John Bode
quelle
1

Der C-Standard definiert für den *Bediener nur zwei Bedeutungen :

  • Indirektionsoperator
  • Multiplikationsoperator

Es ist wahr, dass es für den * Bediener zwei Bedeutungen gibt . Es ist nicht wahr, dass dies die einzigen Bedeutungen des * Tokens sind .

In einer Erklärung wie

int *p;

das *ist kein Operator; Es ist Teil der Syntax einer Deklaration.

Die Deklarationssyntax von C soll die Ausdruckssyntax widerspiegeln, sodass die obige Deklaration als " *pvom Typ int" gelesen werden kann , woraus wir schließen können, dass sie pvom Typ ist int*. Dies ist jedoch keine feste Regel und wird nirgendwo im Standard angegeben. Stattdessen wird die Syntax von Deklarationen unabhängig von der Syntax von Ausdrücken eigenständig definiert. Aus diesem Grund können beispielsweise die meisten Operatoren, die in Ausdrücken vorkommen können, in Deklarationen nicht mit derselben Bedeutung angezeigt werden (es sei denn, sie sind Teil eines Ausdrucks, der Teil der Deklaration ist, wie der +Operator in int array[2+2];).

Ein Fall, in dem das Prinzip "Deklarationsspiegel verwenden" nicht ganz funktioniert, ist:

int arr[10];

wo arr[10]wäre vom Typ, int wenn es existieren würde .

Und tatsächlich gibt es noch eine andere Verwendung des *Tokens. Ein Array-Parameter kann mit deklariert werden [*], um ein Array mit variabler Länge anzugeben. (Dies wurde in C99 hinzugefügt.) Dies *ist weder eine Multiplikation noch eine Dereferenzierung. Ich vermute, es wurde von der Shell-Wildcard-Syntax inspiriert.

Keith Thompson
quelle
Ein größerer Punkt, an dem das Konzept "Deklaration folgt Verwendung" zusammenbricht, ist die Zeigerinitialisierung. Die Wirkung einer Deklaration int *p = 0;ähnelt der Verwendung * p = 0; `, aber die Bedeutungen sind sehr unterschiedlich.
Supercat
@ Supercat: Sicher. Die Initialisierung entspricht immer dem Objekttyp (in diesem Fall int*).
Keith Thompson