Warum existiert der Pfeiloperator (->) in C?

264

Der dot ( .) -Operator wird verwendet, um auf ein Mitglied einer Struktur zuzugreifen, während der Pfeiloperator ( ->) in C verwendet wird, um auf ein Mitglied einer Struktur zuzugreifen, auf das 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 einen Ort im virtuellen Speicher beschreibt, also hat er keine Mitglieder). Es würde also keine Mehrdeutigkeit geben, wenn wir nur den Punktoperator definieren würden, um den Zeiger automatisch zu dereferenzieren, wenn er für einen Zeiger verwendet wird (eine Information, die dem Compiler zur Kompilierungszeit bekannt ist).

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

Askaga
quelle
1
Verwandte: stackoverflow.com/questions/221346/… - auch können Sie überschreiben ->
Krease
16
@ Chris Das ist über C ++, was natürlich einen großen Unterschied macht. Aber da wir darüber sprechen, warum C so entworfen wurde, tun wir so, als wären wir in den 1970er Jahren - bevor es C ++ gab.
Mysticial
5
Meine beste Vermutung ist, dass der Pfeiloperator existiert, um visuell auszudrücken "schau es dir an! Du hast es hier mit einem Zeiger zu tun"
Chris
4
Auf einen Blick finde ich diese Frage sehr seltsam. Nicht alle Dinge sind durchdacht gestaltet. Wenn Sie diesen Stil in Ihrem ganzen Leben beibehalten, wäre Ihre Welt voller Fragen. Die Antwort, die die meisten Stimmen hat, ist wirklich informativ und klar. Aber es trifft nicht den entscheidenden Punkt Ihrer Frage. Folgen Sie dem Stil Ihrer Frage, ich kann zu viele Fragen stellen. Das Schlüsselwort 'int' ist beispielsweise die Abkürzung für 'integer'. Warum ist das Schlüsselwort 'double' nicht auch kürzer?
Junwanghe
1
@junwanghe Diese Frage stellt tatsächlich ein berechtigtes Anliegen dar - warum hat der .Operator eine höhere Priorität als der *Operator? Wenn nicht, könnten wir * ptr.member und var.member haben.
Milleniumbug

Antworten:

358

Ich werde Ihre Frage als zwei Fragen interpretieren: 1) warum ->überhaupt existiert und 2) warum .der Zeiger nicht automatisch dereferenziert wird. Die Antworten auf beide Fragen haben historische Wurzeln.

Warum gibt ->es überhaupt?

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

Die von CRM beschriebene C-Sprache unterschied sich in vielerlei Hinsicht stark von der modernen C. In CRM haben Strukturmitglieder das globale Konzept von implementiert Byte-Offsets , das ohne Typeinschränkungen zu jedem Adresswert 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 awürde für Offset 0 stehen, während nameb für Offset 2 stehen würde (unter intder Annahme, dass Typ 2 und keine Polsterung vorhanden sind). Die Sprache erfordert, dass alle Mitglieder aller Strukturen in der Übersetzungseinheit entweder eindeutige Namen haben oder für denselben Versatzwert stehen. ZB in derselben Übersetzungseinheit könnten 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 es versucht hat, "neu zu definieren" a als Offset 2 und bals Offset 0 .

Und hier ist die -> Operator ins Spiel. Da jeder Name eines Strukturmitglieds 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 übernehmen 5, Offset hinzufügen 2und 42dem intWert an der resultierenden Adresse zuweisen " interpretiert . Dh die oben würde zuweisen 42zuint Wert an der Adresse7 . Beachten Sie, dass sich diese Verwendung von ->nicht um den Typ des Ausdrucks auf der linken Seite kümmerte. Die linke Seite wurde als numerische Adresse mit r-Wert interpretiert (sei es ein Zeiger oder eine ganze Zahl).

Diese Art von Betrug war mit *und nicht möglich. Kombination . Das kannst du nicht machen

(*i).b = 42;

da *iist schon ein ungültiger Ausdruck. Das* Operator stellt, da er von diesem getrennt ist ., strengere Typanforderungen an seinen Operanden. Um diese Einschränkung umgehen zu können, hat CRM den ->Operator eingeführt, der unabhängig vom Typ des linken Operanden ist.

Wie Keith in den Kommentaren feststellte, wird dieser Unterschied zwischen ->und *+ .Kombination von CRM in 7.1.8 als "Lockerung der Anforderung" bezeichnet: Mit Ausnahme der Lockerung der Anforderung, die vom Zeigertyp ist , der AusdruckE1E1−>MOS genau äquivalent zu(*E1).MOS

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

Warum kann .der Zeiger nicht automatisch dereferenziert werden?

Auch in der CRM-Version der Sprache musste der linke Operand des .Operators ein Wert sein . Dies war die einzige Anforderung, die an diesen Operanden gestellt wurde (und das machte ihn anders ->als oben erläutert). Beachten Sie, dass CRM hat nicht erfordern den linken Operanden von .einem Strukturtyp zu haben. Es musste nur ein Wert sein, ein beliebiger Wert. 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 55in einen intWert schreiben, der am Byte-Offset 2 im kontinuierlichen Speicherblock positioniert ist c, obwohl bekannt ist , obwohl der Typ struct Tkein Feld benannt hat b. Der Compiler würde sich überhaupt nicht um den tatsächlichen Typ kümmern c. Alles, was es interessierte, ist dasc war, es sich um einen Wert handelte: eine Art beschreibbaren Speicherblock.

Beachten Sie nun, dass Sie dies getan haben

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

Der Code wird als gültig angesehen (da er sauch ein Wert ist) und der Compiler würde einfach versuchen, Daten in den Zeiger selbst zu schreibens mit Byte-Offset 2 . Unnötig zu Dinge leicht zu einem Speicherüberlauf führen können, aber die Sprache beschäftigte sich nicht mit solchen Angelegenheiten.

Dh in dieser Version der Sprache würde Ihre vorgeschlagene Idee zum Überladen von Operatoren .für Zeigertypen nicht funktionieren: Operator. Zeigertypen bereits eine sehr spezifische Bedeutung, wenn sie mit Zeigern (mit l-Wert-Zeigern oder mit irgendwelchen l-Werten überhaupt) verwendet wurden. Es war zweifellos eine sehr seltsame Funktionalität. Aber es war zu der Zeit da.

Natürlich ist diese seltsame Funktionalität kein sehr starker Grund gegen die Einführung eines überladenen .Operators für Zeiger (wie Sie vorgeschlagen haben) in der überarbeiteten Version von C - K & R C. Aber es wurde nicht getan. Vielleicht gab es zu dieser Zeit einen alten 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 weitere Kopie, möglicherweise mit geringfügigen Unterschieden, befindet sich hier .)

AnT
quelle
10
In Abschnitt 7.1.8 des zitierten C-Referenzhandbuchs heißt es: "Abgesehen von der Lockerung der Anforderung, dass E1 vom Zeigertyp sein muss, entspricht der Ausdruck" E1 -> MOS "genau" (* E1) .MOS ". '. "
Keith Thompson
1
Warum war es *ian Adresse 5 kein Wert eines Standardtyps (int?)? Dann hätte (* i) .b genauso funktioniert.
Random832
5
@Leo: Nun, einige Leute mögen die C-Sprache als übergeordneten Assembler. Zu dieser Zeit in der C-Geschichte war die Sprache tatsächlich ein übergeordneter Assembler.
Am
29
Huh. Dies erklärt, warum viele Strukturen in UNIX (z. B. struct stat) ihren Feldern (z st_mode. B. ) ein Präfix voranstellen .
icktoofay
5
@ perfectm1ng: Es sieht so aus, als ob bell-labs.com von Alcatel-Lucent übernommen wurde und die Originalseiten verschwunden sind. Ich habe den Link zu einer anderen Site aktualisiert, obwohl ich nicht sagen kann, wie lange diese noch aktiv sein wird. Wenn Sie nach dem "Ritchie C Referenzhandbuch" googeln, wird das Dokument normalerweise gefunden.
Am
46

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 Zeiger auf Struktur enthält, die Zeiger auf Struktur enthält ... Diese beiden sind äquivalent:

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

a->b->c->d

Aber der zweite ist deutlich besser lesbar. Der Pfeiloperator hat die höchste Priorität (genau wie der Punkt) und wird von links nach rechts zugeordnet. Ich denke, dies ist klarer als die Verwendung des Punktoperators sowohl für Zeiger auf struct als auch auf struct, da wir den Typ aus dem Ausdruck kennen, ohne auf die Deklaration achten zu müssen, die sich sogar in einer anderen Datei befinden könnte.

effeffe
quelle
2
Bei verschachtelten Datentypen, die sowohl Strukturen als auch Zeiger auf Strukturen enthalten, kann dies die Sache erschweren, da Sie über die Auswahl des richtigen Operators für jeden Zugriff auf Untermitglieder nachdenken müssen. Sie könnten am Ende mit ab-> c-> d oder a-> bc-> d enden (ich hatte dieses Problem bei der Verwendung der Freetype-Bibliothek - ich musste den Quellcode ständig nachschlagen). Dies erklärt auch nicht, warum es nicht möglich wäre, den Compiler den Zeiger beim Umgang mit Zeigern automatisch dereferenzieren zu lassen.
Askaga
3
Obwohl die von Ihnen angegebenen Fakten korrekt sind, beantworten sie meine ursprüngliche Frage in keiner Weise. Sie erklären die Gleichheit von a-> und * (a). Notationen (die bereits mehrfach in anderen Fragen erklärt wurden) sowie eine vage Aussage darüber, dass das Sprachdesign etwas willkürlich ist. Ich fand Ihre Antwort nicht sehr hilfreich, daher die Ablehnung.
Askaga
16
@effeffe, das OP sagt, dass die Sprache leicht a.b.c.dals interpretiert werden könnte (*(*(*a).b).c).d, was den ->Operator unbrauchbar macht. Die Version ( a.b.c.d) des OP ist also gleichermaßen lesbar (im Vergleich zu a->b->c->d). Deshalb beantwortet Ihre Antwort die Frage des OP nicht.
Shahbaz
4
@Shahbaz Wenn das der Fall für einen Java - Programmierer sein kann, werden ein C / C ++ Programmierer verstehen a.b.c.dund a->b->c->dwie zwei sehr verschiedene Dinge: Das ist zunächst ein einzelner Speicherzugriff auf ein verschachteltes Unterobjekt (es gibt nur ein einziges Speicherobjekt in diesem Fall ), der zweite ist drei Speicherzugriffe, die Zeiger durch vier wahrscheinlich unterschiedliche Objekte verfolgen. Das ist ein großer Unterschied im Speicherlayout, und ich glaube, dass C richtig ist, um diese beiden Fälle sehr sichtbar zu unterscheiden.
cmaster
2
@ Shahbaz Ich meinte nicht, dass sie als Beleidigung der Java-Programmierer einfach an eine Sprache mit vollständig impliziten Zeigern gewöhnt sind. Wäre ich als Java-Programmierer aufgewachsen, würde ich wahrscheinlich genauso denken ... Wie auch immer, ich denke tatsächlich, dass die Operatorüberladung, die wir in C sehen, nicht optimal ist. Ich gebe jedoch zu, dass wir alle von den Mathematikern verwöhnt wurden, die ihre Operatoren für so ziemlich alles großzügig überlasten. Ich verstehe auch ihre Motivation, da der Satz verfügbarer Symbole eher begrenzt ist. Ich denke, am Ende ist es nur die Frage, wo Sie die Grenze ziehen ...
cmaster - wieder einsetzen Monica
19

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 mit einem Zeiger arbeitet, genau wie wenn der Compiler nicht zulässt, dass Sie zwei inkompatible Typen mischen.

Mukunda
quelle
4
Dies ist die einfache und richtige Antwort. C versucht meistens, eine Überlastung zu vermeiden, was IMO eines der besten Dinge an C. ist
Jforberg
10
Viele Dinge in C sind mehrdeutig und verschwommen. Es gibt implizite Typkonvertierungen, mathematische Operatoren sind überladen, die verkettete Indizierung macht etwas völlig anderes, je nachdem, ob Sie ein mehrdimensionales Array oder ein Array von Zeigern indizieren, und alles könnte ein Makro sein, das alles verbirgt (die Namenskonvention in Großbuchstaben hilft dort, aber C nicht) t).
PSkocik