Warum wurde die C-Syntax für Arrays, Zeiger und Funktionen so entworfen?

16

Nachdem ich so viele ähnliche Fragen gesehen (und gestellt!) Habe

Was int (*f)(int (*a)[5])bedeutet in C?

und selbst wenn ich sehe , dass sie ein Programm erstellt haben , das den Leuten hilft, die C-Syntax zu verstehen, frage ich mich:

Warum wurde die Syntax von C so entworfen?

Wenn ich beispielsweise Zeiger entwerfe, würde ich "einen Zeiger auf ein 10-Elemente-Array von Zeigern" in übersetzen

int*[10]* p;

und nicht

int* (*p)[10];

was meiner Meinung nach die meisten Leute zustimmen würden, ist viel weniger einfach.

Also frage ich mich, warum die, äh, nicht intuitive Syntax? Gab es ein bestimmtes Problem, das die Syntax löst (vielleicht eine Mehrdeutigkeit?), Von dem ich nichts weiß?

Mehrdad
quelle
2
Sie wissen, dass es auf diese und solche Fragen keine wirkliche Antwort gibt. Richtig? Was Sie bekommen, sind nur Vermutungen.
BЈовић
7
@VJo - es kann durchaus eine "echte" (dh objektive) Antwort geben - Autoren und Normenausschüsse haben viele dieser Entscheidungen ausdrücklich begründet (oder zumindest erklärt).
Detly
Ich glaube nicht, dass Ihre vorgeschlagene Syntax notwendigerweise mehr oder weniger "intuitiv" ist als die C-Syntax. C ist was es ist; Sobald Sie es gelernt haben, werden Sie diese Fragen nie wieder haben. Wenn du es nicht gelernt hast ... na ja, vielleicht ist das das eigentliche Problem.
Caleb
1
@Caleb: Komisch, wie du das so einfach festgestellt hast, weil ich es gelernt habe und immer noch diese Frage hatte ...
Mehrdad
1
Der cdeclBefehl ist sehr praktisch, um komplexe C-Deklarationen zu dekodieren. Es gibt auch ein Webinterface bei cdecl.org .
Keith Thompson

Antworten:

16

Mein Verständnis der Geschichte davon ist, dass es auf zwei Hauptpunkten basiert ...

Erstens zogen es die Sprachautoren vor, die Syntax variablenzentriert anstatt typzentriert zu gestalten. Das heißt, sie wollten, dass ein Programmierer sich die Deklaration ansieht und denkt: "Wenn ich den Ausdruck schreibe *func(arg), führt das zu einem int; wenn ich schreibe, *arg[N]habe ich einen Gleitkomma" anstatt " funcmuss ein Zeiger auf eine Funktion sein, die dies ausführt und die Rückkehr , dass “.

Der C-Eintrag auf Wikipedia behauptet, dass:

Ritchies Idee war es, Identifikatoren in Kontexten zu deklarieren, die ihrer Verwendung ähneln: "Deklaration spiegelt Verwendung wider".

... unter Berufung auf Seite 122 von K & R2, die ich leider nicht vorlegen muss, um das erweiterte Angebot für Sie zu finden.

Zweitens ist es wirklich sehr, sehr schwierig, eine Syntax für die Deklaration zu finden, die konsistent ist, wenn Sie mit beliebigen Indirektionsebenen arbeiten. Ihr Beispiel eignet sich möglicherweise gut, um den Typ auszudrücken, den Sie sich dort spontan ausgedacht haben. Skaliert es sich jedoch auf eine Funktion, die einen Zeiger auf ein Array dieser Typen nimmt und ein anderes abscheuliches Durcheinander zurückgibt? (Vielleicht schon, aber hast du nachgesehen? Kannst du es beweisen? ).

Denken Sie daran, dass ein Teil des Erfolgs von C auf der Tatsache beruht, dass Compiler für viele verschiedene Plattformen geschrieben wurden. Es wäre daher möglicherweise besser gewesen, ein gewisses Maß an Lesbarkeit zu ignorieren, um das Schreiben von Compilern zu vereinfachen.

Trotzdem bin ich kein Experte für Sprachgrammatik oder Compilerschreiben. Aber ich weiß genug, um zu wissen, dass es viel zu wissen gibt;)

tückisch
quelle
2
"Compiler einfacher zu schreiben" ... außer C ist bekannt dafür, dass es schwer zu analysieren ist (nur von C ++ übertroffen).
Jan Hudec
1
@JanHudec - Nun ... ja. Das ist keine wasserdichte Aussage. Aber während es unmöglich ist, C als kontextfreie Grammatik zu analysieren, hört dies auf, der schwierige Schritt zu sein, sobald eine Person einen Weg gefunden hat, es zu analysieren. Und die Tatsache ist, es war aufgrund Menschen in seinen frühen Tagen fruchtbarer Lage, Compiler leicht bang, so K & R müssen ein gewisses Gleichgewicht getroffen haben. (In Richard Gabriel berüchtigt The Rise of „Schlimmer ist besser“ , er für selbstverständlich nimmt - und beklagt -. Die Tatsache , dass es einfach einen C - Compiler für eine neue Plattform schreiben)
detly
Ich bin übrigens froh, dass ich hier korrigiert werden kann - ich weiß nicht so viel über Parsen und Grammatik. Ich gehe eher auf historische Tatsachen ein.
Detly
12

Viele der Merkwürdigkeiten der C-Sprache lassen sich durch die Funktionsweise der Computer bei ihrer Entwicklung erklären. Da der Speicherplatz sehr begrenzt war, war es sehr wichtig, die Größe der Quellcodedateien selbst zu minimieren . Die Programmierpraxis in den 70er und 80er Jahren bestand darin, sicherzustellen, dass der Quellcode so wenig Zeichen wie möglich enthielt und möglichst keine übermäßigen Quellcode-Kommentare.

Das ist heute natürlich lächerlich, da der Speicherplatz auf den Festplatten so gut wie unbegrenzt ist. Aber es ist ein Teil des Grundes, warum C im Allgemeinen eine so seltsame Syntax hat.


In Bezug auf Array-Zeiger sollte Ihr zweites Beispiel lauten int (*p)[10];(ja, die Syntax ist sehr verwirrend). Ich würde das vielleicht als "int pointer to array of ten" lesen ... das macht etwas Sinn. Ohne die Klammer würde der Compiler es stattdessen als ein Array von zehn Zeigern interpretieren, was der Deklaration eine ganz andere Bedeutung geben würde.

Da sowohl Array-Zeiger als auch Funktionszeiger in C eine ziemlich undurchsichtige Syntax haben, ist es sinnvoll, die Unheimlichkeit wegzuschreiben. Vielleicht so:

Dunkles Beispiel:

int func (int (*arr_ptr)[10])
{
  return 0;
}

int main()
{
  int array[10];
  int (*arr_ptr)[10]  = &array;
  int (*func_ptr)(int(*)[10]) = &func;

  func_ptr(arr_ptr);
}

Nicht obskures, gleichwertiges Beispiel:

typedef int array_t[10];
typedef int (*funcptr_t)(array_t*);


int func (array_t* arr_ptr)
{
  return 0;
}

int main()
{
  int        array[10];
  array_t*   arr_ptr  = &array; /* non-obscure array pointer */
  funcptr_t  func_ptr = &func;  /* non-obscure function pointer */

  func_ptr(arr_ptr);
}

Wenn Sie sich mit Arrays von Funktionszeigern beschäftigen, können die Dinge noch dunkler werden. Oder die dunkelste von allen: Funktionen, die Funktionszeiger zurückgeben (mild nützlich). Wenn Sie für solche Dinge keine Typedefs verwenden, werden Sie schnell verrückt.


quelle
Ah, endlich eine vernünftige Antwort. :-) Ich bin gespannt, wie die jeweilige Syntax die Quellcode-Größe tatsächlich verkleinern würde, aber es ist auf jeden Fall eine plausible Idee und sinnvoll. Vielen Dank. +1
Mehrdad
Ich würde sagen, es ging weniger um die Größe des Quellcodes als vielmehr um das Schreiben des Compilers, aber definitiv um +1 für "typdef away the weirdness". Meine geistige Gesundheit verbesserte sich dramatisch, als ich merkte, dass ich das tun konnte.
Detly
2
[Zitieren benötigt] auf der Quellcode-Größe Sache. Ich habe noch nie von einer solchen Einschränkung gehört (obwohl es vielleicht etwas ist, was "jeder weiß").
Sean McMillan
1
Nun, ich habe in den 70er Jahren Programme in COBOL, Assembler, CORAL und PL / 1 auf IBM-, DEC- und XEROX-Kits codiert und bin NIEMALS auf eine Beschränkung der Quellcode-Größe gestoßen. Einschränkungen in Bezug auf die Array-Größe, die Größe der ausführbaren Datei und den Programmnamen, jedoch niemals die Größe des Quellcodes.
James Anderson
1
@ Sean McMillan: Ich glaube nicht, dass die Größe des Quellcodes eine Einschränkung war (bedenken Sie, dass zu dieser Zeit ausführliche Sprachen wie Pascal sehr beliebt waren). Und selbst wenn dies der Fall gewesen wäre, wäre es meiner Meinung nach sehr einfach gewesen, den Quellcode zu parsen und lange Schlüsselwörter durch kurze Ein-Byte-Codes zu ersetzen (wie dies beispielsweise bei einigen Basic-Interpretern der Fall war). Daher finde ich das Argument "C ist knapp, weil es in einer Zeit erfunden wurde, in der weniger Speicher verfügbar war" etwas schwach.
Giorgio
7

Es ist ziemlich einfach: int *pbedeutet, dass *pes sich um ein int handelt; int a[5]bedeutet das a[i]ist ein int.

int (*f)(int (*a)[5])

Bedeutet, dass *fes sich bei einer Funktion *aum ein Array mit fünf Ganzzahlen handelt. Dies fgilt auch für eine Funktion, die einen Zeiger auf ein Array mit fünf Ganzzahlen nimmt und int zurückgibt. In C ist es jedoch nicht sinnvoll, einen Zeiger auf ein Array zu übergeben.

C-Deklarationen werden sehr selten so kompliziert.

Außerdem können Sie mit typedefs Folgendes klären:

typedef int vec5[5];
int (*f)(vec5 *a);
Kevin Cline
quelle
4
Entschuldigung, wenn dies unhöflich klingt (ich meine es nicht so), aber ich denke, Sie haben den ganzen Punkt der Frage verpasst ...: \
Mehrdad
2
@Mehrdad: Ich kann dir nicht sagen, was Kernighan und Ritchie im Sinn hatten. Ich habe dir die Logik hinter der Syntax erklärt. Ich kenne die meisten Leute nicht, aber ich denke nicht, dass Ihre vorgeschlagene Syntax klarer ist.
Kevin Cline
Ich stimme zu - es ist ungewöhnlich, eine so komplizierte Erklärung zu sehen.
Caleb
Der Entwurf von C-Deklarationen ist älter typedefals const, volatileund die Fähigkeit, Dinge innerhalb von Deklarationen zu initialisieren. Viele der lästigen Zweideutigkeiten der Deklarationssyntax (zB ob int const *p, *q;binden sollte constauf die Art oder die declarand) konnten nicht in der Sprache auftreten , wie ursprünglich vorgesehen. Ich wünschte, die Sprache hätte einen Doppelpunkt zwischen dem Typ und dem Deklarationszeichen eingefügt, erlaubte aber das Weglassen, wenn eingebaute "Reserviert-Wort" -Typen ohne Qualifizierer verwendet wurden. Die Bedeutung von int: const *p,*q;und int const *: p,*q;wäre klar gewesen.
Supercat
3

Ich denke, Sie müssen * [] als Operatoren betrachten, die an eine Variable angehängt sind. * wird vor eine Variable geschrieben, [] danach.

Lesen wir den Typ-Ausdruck

int* (*p)[10];

Das innerste Element ist daher p, eine Variable

p

bedeutet: p ist eine Variable.

Bevor die Variable ein * enthält, wird der Operator * immer vor den Ausdruck gesetzt, auf den er sich bezieht.

(*p)

bedeutet: Variable p ist ein Zeiger. Ohne das () hätte der Operator [] auf der rechten Seite eine höhere Priorität, d. H

**p[]

würde analysiert werden als

*(*(p[]))

Der nächste Schritt ist []: Da es kein weiteres () gibt, hat [] daher eine höhere Priorität als das äußere *

(*p)[]

bedeutet: (Variable p ist ein Zeiger) auf ein Array. Dann haben wir den zweiten *:

* (*p)[]

bedeutet: ((Variable p ist ein Zeiger) auf ein Array) von Zeigern

Schließlich haben Sie den Operator int (einen Typnamen), der die niedrigste Priorität hat:

int* (*p)[]

bedeutet: (((Variable p ist ein Zeiger) auf ein Array) von Zeigern) auf eine ganze Zahl.

Das gesamte System basiert also auf Typausdrücken mit Operatoren, und jeder Operator hat seine eigenen Vorrangregeln. Dadurch können sehr komplexe Typen definiert werden.

Giorgio
quelle
0

Es ist nicht so schwer, wenn man anfängt zu denken, und C war nie eine sehr einfache Sprache. Und ist int*[10]* pwirklich nicht einfacher als int* (*p)[10] Und welche Art von k wäre inint*[10]* p, k;

Dainius
quelle
2
k wäre eine fehlgeschlagene Codeüberprüfung, ich kann herausfinden, was der Compiler tun wird, ich kann sogar belästigt werden, aber ich kann nicht herausfinden, was der Programmierer beabsichtigt hat - fehlschlagen ............
mattnz
und warum k Codeüberprüfung fehlschlagen würde?
Dainius
1
weil der Code nicht lesbar und nicht wartbar ist. Der Code ist nicht korrekt, offensichtlich korrekt und es ist wahrscheinlich, dass er auch während der Wartung korrekt bleibt. Die Tatsache, dass Sie nach dem Typ k fragen müssen, ist ein Zeichen dafür, dass der Code diese grundlegenden Anforderungen nicht erfüllt.
Mattnz
1
Grundsätzlich gibt es 3 (in diesem Fall) Variablendeklarationen unterschiedlichen Typs in derselben Zeile, z. B. int * p, int i [10] und int k. Das ist inakzeptabel. Mehrere Deklarationen desselben Typs sind zulässig, vorausgesetzt, die Variablen haben eine Beziehung, z. B. int width, height, depth. Denken Sie daran, dass viele Leute mit int * p programmieren, also was ist ich in 'int * p, i;'.
Mattnz
1
Was @mattnz zu sagen versucht, ist, dass Sie so schlau sein können, wie Sie möchten, aber es ist alles bedeutungslos, wenn Ihre Absicht nicht offensichtlich ist und / oder Ihr Code schlecht geschrieben / unlesbar ist. Diese Art von Dingen führt oft zu fehlerhaftem Code und Zeitverschwendung. Plus, pointer to intund intsind nicht einmal der gleiche Typ, so sollten sie separat deklariert werden. Zeitraum. Hören Sie auf den Mann. Er hat 18k Vertreter aus einem Grund.
Braden Best