wake-up-neo.com

Warum existiert der Pfeil (->) Operator in C?

Der Punkt (.) Operator wird verwendet, um auf ein Mitglied einer Struktur zuzugreifen, während der Pfeil Operator (->) in C wird verwendet, um auf ein Mitglied einer Struktur zuzugreifen, auf die der betreffende Zeiger verweist.

Der Zeiger selbst hat keine Mitglieder, auf die mit dem Punktoperator zugegriffen werden kann (es ist eigentlich nur eine Zahl, die eine Position im virtuellen Speicher beschreibt, sodass er keine Mitglieder hat). Es würde also keine Mehrdeutigkeit geben, wenn wir nur den Punktoperator so definieren würden, dass der Zeiger automatisch dereferenziert wird, wenn er für einen Zeiger verwendet wird (eine Information, die dem Compiler zur Kompilierungszeit bekannt ist).

Warum haben die Sprachentwickler beschlossen, die Dinge durch Hinzufügen dieses scheinbar unnötigen Operators komplizierter zu gestalten? Was ist die große Designentscheidung?

243
Askaga

Ich interpretiere Ihre Frage als zwei Fragen: 1) warum -> Überhaupt existiert und 2) warum . Den Zeiger nicht automatisch dereferenziert. Die Antworten auf beide Fragen haben historische Wurzeln.

Warum gibt es überhaupt ->?

In einer der allerersten Versionen der C-Sprache (die ich als CRM für " C Reference Manual " bezeichnen werde, die im Mai 1975 mit der 6. Edition von Unix geliefert wurde) hatte operator -> Sehr exklusive Bedeutung, nicht gleichbedeutend mit der Kombination * und .

Die von CRM beschriebene C-Sprache unterschied sich in vielerlei Hinsicht stark von der modernen C-Sprache. In CRM-Strukturelementen wurde das globale Konzept Byte-Offset implementiert, das zu jedem Adresswert ohne Typeinschränkungen hinzugefügt werden kann. Das heißt Alle Namen aller Strukturmitglieder hatten eine unabhängige globale Bedeutung (und mussten daher eindeutig sein). Zum Beispiel könnten Sie deklarieren

struct S {
  int a;
  int b;
};

und name a würde für Offset 0 stehen, während name b für Offset 2 stehen würde (vorausgesetzt int Typ von Größe 2 und ohne Auffüllung). Die Sprache, die für alle Mitglieder aller Strukturen in der Übersetzungseinheit erforderlich ist, hat entweder eindeutige Namen oder steht für denselben Versatzwert. Z.B. in der gleichen Übersetzungseinheit können Sie zusätzlich deklarieren

struct X {
  int a;
  int x;
};

und das wäre OK, da der Name a durchweg für Offset 0 stehen würde. Aber diese zusätzliche Deklaration

struct Y {
  int b;
  int a;
};

wäre formal ungültig, da versucht wurde, a als Offset 2 und b als Offset 0 "neu zu definieren".

Und hier kommt der Operator -> Ins Spiel. Da jeder Strukturmitgliedsname eine eigene autarke globale Bedeutung hatte, unterstützte die Sprache solche Ausdrücke

int i = 5;
i->b = 42;  /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */

Die erste Zuweisung wurde vom Compiler als "Adresse 5 Nehmen, Offset 2 Hinzufügen und 42 Dem Wert int an der resultierenden Adresse zuweisen" interpretiert ". Das heißt Das obige würde 42 dem Wert von int an der Adresse 7 zuweisen. Beachten Sie, dass sich diese Verwendung von -> Nicht für den Typ des Ausdrucks auf der linken Seite interessierte. Die linke Seite wurde als eine numerische R-Wert-Adresse interpretiert (sei es ein Zeiger oder eine Ganzzahl).

Diese Art von Trick war mit der Kombination * Und . Nicht möglich. Das könntest du nicht tun

(*i).b = 42;

da *i bereits ein ungültiger Ausdruck ist. Der Operator * Stellt strengere Typanforderungen an seinen Operanden, da er von . Getrennt ist. Um diese Einschränkung zu umgehen, wurde in CRM der Operator -> Eingeführt, der vom Typ des linken Operanden unabhängig ist.

Wie Keith in den Kommentaren feststellte, ist dieser Unterschied zwischen der Kombination -> Und * + . Das, was CRM in 7.1.8 als "Lockerung der Anforderung" bezeichnet: = Abgesehen von der Einschränkung, dass E1 Vom Zeigertyp sein muss, entspricht der Ausdruck E1−>MOS Genau (*E1).MOS

Später wurden in K & R C viele Funktionen, die ursprünglich in CRM beschrieben wurden, erheblich überarbeitet. Die Idee von "Strukturelement als globaler Offset-Bezeichner" wurde vollständig entfernt. Und die Funktionalität des Operators -> Wurde vollständig identisch mit der Funktionalität der Kombination * Und ..

Warum kann . Den Zeiger nicht automatisch dereferenzieren?

Auch in der CRM-Version der Sprache musste der linke Operand des Operators . Ein lWert sein. Das war die only Anforderung, die an diesen Operanden gestellt wurde (und das war der Unterschied zu ->, Wie oben erläutert). Beachten Sie, dass CRM nicht für den linken Operanden von . Einen Strukturtyp benötigt. Es musste nur ein lWert sein, any lWert. Dies bedeutet, dass Sie in der CRM-Version von C Code wie diesen schreiben können

struct S { int a, b; };
struct T { float x, y, z; };

struct T c;
c.b = 55;

In diesem Fall würde der Compiler 55 In einen Wert von int schreiben, der bei Byte-Offset 2 im fortlaufenden Speicherblock mit der Bezeichnung c positioniert ist, obwohl Typ struct T hatte kein Feld mit dem Namen b. Der Compiler würde sich überhaupt nicht um den tatsächlichen Typ von c kümmern. Alles, was es interessierte, war, dass c ein Wert war: eine Art beschreibbarer Speicherblock.

Beachten Sie nun, dass Sie dies getan haben

S *s;
...
s.b = 42;

der Code wird als gültig betrachtet (da s auch ein lWert ist) und der Compiler versucht einfach, Daten in den Zeiger s selbst zu schreiben, bei Byte -offset 2. Unnötig zu erwähnen, dass solche Dinge leicht zu einem Speicherüberlauf führen können, aber die Sprache hat sich nicht mit solchen Dingen befasst.

Das heißt In dieser Version der Sprache würde Ihre vorgeschlagene Idee, den Operator . für Zeigertypen zu überladen, nicht funktionieren: Der Operator . hatte bereits eine sehr spezifische Bedeutung, wenn er mit Zeigern (mit lvalue-Zeigern oder mit lvalues ​​bei) verwendet wurde alle). Es war zweifellos eine sehr seltsame Funktionalität. Aber es war zu der Zeit da.

Natürlich ist diese seltsame Funktionalität kein besonders wichtiger Grund, in der überarbeiteten Version von C - K & R C einen überladenen . - Operator für Zeiger einzuführen (wie Sie vorgeschlagen haben). Vielleicht gab es zu dieser Zeit einen älteren Code in der CRM-Version von C, der unterstützt werden musste.

(Die URL für das C-Referenzhandbuch von 1975 ist möglicherweise nicht stabil. Eine andere Kopie mit möglicherweise geringfügigen Unterschieden ist hier .)

330
AnT

Abgesehen von historischen (guten und bereits gemeldeten) Gründen gibt es auch ein kleines Problem mit der Priorität von Operatoren: Der Punktoperator hat eine höhere Priorität als der Sternoperator. Wenn Sie also eine Struktur haben, die einen Zeiger auf eine Struktur enthält, die einen Zeiger auf eine Struktur enthält, ... Diese beiden sind äquivalent:

(*(*(*a).b).c).d

a->b->c->d

Aber der zweite ist deutlich lesbarer. Pfeiloperator hat die höchste Priorität (nur als Punkt) und ordnet von links nach rechts zu. Ich denke, dies ist klarer, als den Punktoperator sowohl für Zeiger auf struct als auch struct zu verwenden, da wir den Typ aus dem Ausdruck kennen, ohne auf die Deklaration schauen zu müssen, die sich sogar in einer anderen Datei befinden könnte.

38
effeffe

C macht auch einen guten Job darin, nichts mehrdeutig zu machen.

Sicher, der Punkt könnte überladen sein, um beides zu bedeuten, aber der Pfeil stellt sicher, dass der Programmierer weiß, dass er auf einem Zeiger arbeitet, genau wie wenn der Compiler nicht zulässt, dass Sie zwei inkompatible Typen mischen.

19
mukunda