Ich habe heute gerade ein Bild gesehen und denke, ich würde mich über Erklärungen freuen. Also hier ist das Bild:
Ich fand das verwirrend und fragte mich, ob solche Codes jemals praktisch sind. Ich habe das Bild gegoogelt und ein anderes Bild in diesem reddit-Eintrag gefunden, und hier ist das Bild:
Also ist dieses "spiralförmige Lesen" etwas Gültiges? Analysieren C-Compiler auf diese Weise?
Es wäre großartig, wenn es einfachere Erklärungen für diesen seltsamen Code gäbe.
Kann diese Art von Codes, abgesehen von allem, nützlich sein? Wenn ja, wo und wann?
Es gibt eine Frage zur "Spiralregel", aber ich frage nicht nur, wie sie angewendet wird oder wie Ausdrücke mit dieser Regel gelesen werden. Ich bezweifle die Verwendung solcher Ausdrücke und die Gültigkeit der Spiralregel. In Bezug auf diese sind einige nette Antworten bereits veröffentlicht.
f
als ein Array von Zeigern auf Funktionen, die jedes Argument annehmen könnten. Wenn es so wärevoid (*(*f[])(void))(void);
, dann wären es Funktionen, die keine ArgumenteAntworten:
Es gibt eine Regel namens "Clockwise / Spiral Rule" , um die Bedeutung einer komplexen Deklaration zu ermitteln.
Aus c-faq :
Sie können den obigen Link für Beispiele überprüfen.
Beachten Sie auch, dass es zur Unterstützung auch eine Website mit dem Namen gibt:
http://www.cdecl.org
Sie können eine C-Erklärung eingeben, die ihre englische Bedeutung angibt. Zum
es gibt aus:
BEARBEITEN:
Wie in den Kommentaren von Random832 ausgeführt , behandelt die Spiralregel kein Array von Arrays und führt bei (den meisten) dieser Deklarationen zu einem falschen Ergebnis. Zum Beispiel
int **x[1][2];
ignoriert die Spiralregel die Tatsache, dass[]
einen höheren Vorrang hat*
.Wenn Sie sich vor einem Array von Arrays befinden, können Sie zunächst explizite Klammern hinzufügen, bevor Sie die Spiralregel anwenden. Beispielsweise:
int **x[1][2];
istint **(x[1][2]);
aufgrund der Priorität dasselbe wie (auch gültiges C) und die Spiralregel liest es dann korrekt als "x ist ein Array 1 von Array 2 von Zeiger auf Zeiger auf int", was die korrekte englische Deklaration ist.Beachten Sie, dass dieses Problem auch in dieser Antwort von James Kanze behandelt wurde (auf Haccks in den Kommentaren hingewiesen ).
quelle
Die "Spiral" -Regel fällt aus den folgenden Vorrangregeln heraus:
Die Operatoren für Index-
[]
und Funktionsaufrufe()
haben eine höhere Priorität als unary*
und werden daher*f()
als*(f())
und*a[]
als analysiert*(a[])
.Wenn Sie also einen Zeiger auf ein Array oder einen Zeiger auf eine Funktion wünschen, müssen Sie die explizit
*
mit dem Bezeichner wie in(*a)[]
oder gruppieren(*f)()
.Dann erkennen Sie das
a
undf
können kompliziertere Ausdrücke sein als nur Bezeichner; inT (*a)[N]
,a
könnte ein einfacher Bezeichner sein, oder es könnte ein Funktionsaufruf wie(*f())[N]
(a
->f()
) sein, oder es könnte ein Array wie(*p[M])[N]
, (a
->p[M]
) sein, oder es könnte ein Array von Zeigern auf Funktionen wie(*(*p[M])())[N]
(a
->(*p[M])()
) sein, etc.Es wäre schön, wenn der Indirektionsoperator
*
postfix statt unary wäre, was das Lesen von Deklarationen von links nach rechts etwas erleichtern würde (void f[]*()*();
fließt definitiv besser alsvoid (*(*f[])())()
), aber das ist es nicht.Wenn Sie auf eine solche haarige Deklaration stoßen, suchen Sie zunächst den Bezeichner ganz links und wenden Sie die oben genannten Prioritätsregeln an, indem Sie sie rekursiv auf alle Funktionsparameter anwenden:
Die
signal
Funktion in der Standardbibliothek ist wahrscheinlich das Muster für diese Art von Wahnsinn:An diesem Punkt sagen die meisten Leute "benutze typedefs", was sicherlich eine Option ist:
Aber...
Wie würden Sie verwenden
f
in einem Ausdruck? Sie wissen, dass es sich um ein Array von Zeigern handelt, aber wie verwenden Sie es, um die richtige Funktion auszuführen? Sie müssen die Typedefs durchgehen und die richtige Syntax herausfinden. Im Gegensatz dazu ist die "nackte" Version ziemlich augenfällig, aber sie sagt Ihnen genau, wie man sief
in einem Ausdruck verwendet ((*(*f[i])())();
vorausgesetzt, keine der beiden Funktionen benötigt Argumente).quelle
f
Verzögerungsbaum, der den Vorrang erklärt ... aus irgendeinem Grund bekomme ich immer einen Kick von ASCII-Kunst, besonders wenn es darum geht, Dinge zu erklären :)void
in Klammern für Funktionen verwenden, andernfalls können beliebige Argumente verwendet werden.In C spiegelt die Deklaration die Verwendung wider - so wird sie im Standard definiert. Die Erklärung:
Ist eine Behauptung, dass der Ausdruck
(*(*f[i])())()
ein Ergebnis vom Typ erzeugtvoid
. Was bedeutet:f
muss ein Array sein, da Sie es indizieren können:Die Elemente von
f
müssen Zeiger sein, da Sie sie dereferenzieren können:Diese Zeiger müssen Zeiger auf Funktionen sein, die keine Argumente annehmen, da Sie sie aufrufen können:
Die Ergebnisse dieser Funktionen müssen auch Zeiger sein, da Sie sie dereferenzieren können:
Diese Zeiger müssen auch Zeiger auf Funktionen sein, die keine Argumente annehmen, da Sie sie aufrufen können:
Diese Funktionszeiger müssen zurückkehren
void
Die „Spiralregel“ ist nur eine Mnemonik, die eine andere Art bietet, dasselbe zu verstehen.
quelle
vector< function<function<void()>()>* > f
, vor allem wenn man dasstd::
s hinzufügt . (Aber gut, das Beispiel ist erfunden ...f :: [IORef (IO (IO ()))]
sieht sogar komisch aus.)a[x]
gibt an, dass der Ausdrucka[i]
gültig ist, wenni >= 0 && i < x
. Lässta[]
die Größe nicht spezifiziert und ist daher identisch mit*a
: Es zeigt an, dass der Ausdrucka[i]
(oder gleichwertig*(a + i)
) für einen bestimmten Bereich von gültig isti
.(*f[])()
ist ein Typ, den Sie indizieren, dann dereferenzieren und dann aufrufen können. Es handelt sich also um ein Array von Zeigern auf Funktionen.Das Anwenden der Spiralregel oder das Verwenden von cdecl sind nicht immer gültig. Beides schlägt in einigen Fällen fehl. Die Spiralregel funktioniert in vielen Fällen, ist aber nicht universell .
Um komplexe Deklarationen zu entschlüsseln, beachten Sie diese beiden einfachen Regeln:
Lesen Sie die Erklärungen immer von innen nach außen : Beginnen Sie gegebenenfalls in der innersten Klammer. Suchen Sie die Kennung, die deklariert wird, und entschlüsseln Sie die Deklaration von dort aus.
Wenn es eine Auswahl gibt, bevorzugen Sie immer
[]
und()
mehr*
: Wenn*
vor dem Bezeichner steht und ihm[]
folgt, repräsentiert der Bezeichner ein Array, keinen Zeiger. Wenn*
der Bezeichner vor und()
nach ihm steht, repräsentiert der Bezeichner eine Funktion und keinen Zeiger. (Klammern können immer verwendet werden, um die normale Priorität von[]
und()
über zu überschreiben*
.)Diese Regel beinhaltet tatsächlich das Zick- Zack- Verfahren von einer Seite des Bezeichners zur anderen.
Entschlüsseln Sie nun eine einfache Erklärung
Regel anwenden:
Lassen Sie uns die komplexe Deklaration wie entschlüsseln
durch Anwendung der oben genannten Regeln:
Hier ist ein GIF, das zeigt, wie Sie vorgehen (klicken Sie auf das Bild, um es zu vergrößern):
Die hier genannten Regeln stammen aus dem Buch C Programming A Modern Approach von KN KING .
quelle
char (x())[5]
sollte zu einem Syntaxfehler führen, aber cdecl parse es als: deklarierex
als Funktion, die Array 5 von zurückgibtchar
.Es ist nur eine "Spirale", weil es in dieser Deklaration nur einen Operator auf jeder Seite in jeder Ebene von Klammern gibt. Die Behauptung, dass Sie "spiralförmig" vorgehen, würde im Allgemeinen bedeuten, dass Sie in der Deklaration zwischen Arrays und Zeigern wechseln,
int ***foo[][][]
wenn in Wirklichkeit alle Array-Ebenen vor einer der Zeiger-Ebenen liegen.quelle
Ich bezweifle, dass solche Konstruktionen im wirklichen Leben von Nutzen sein können. Ich verabscheue sie sogar als Interviewfragen für die regulären Entwickler (wahrscheinlich in Ordnung für Compiler-Autoren). Stattdessen sollten typedefs verwendet werden.
quelle
Als zufälliges Trivia-Faktoid ist es vielleicht amüsant zu wissen, dass es auf Englisch ein tatsächliches Wort gibt, das beschreibt, wie C-Deklarationen gelesen werden: Boustrophedonisch , dh abwechselnd von rechts nach links mit von links nach rechts.
Referenz: Van der Linden, 1994 - Seite 76
quelle
In Bezug auf die Nützlichkeit davon sehen Sie bei der Arbeit mit Shellcode viel von diesem Konstrukt:
Obwohl nicht ganz so syntaktisch kompliziert, kommt dieses spezielle Muster häufig vor.
Vollständigeres Beispiel in dieser SO-Frage.
Während die Nützlichkeit im Ausmaß des Originalbilds fraglich ist (ich würde vorschlagen, dass jeder Produktionscode drastisch vereinfacht werden sollte), gibt es einige syntaktische Konstrukte, die ziemlich häufig auftauchen.
quelle
Die Erklärung
ist nur eine obskure Art zu sagen
mit
In der Praxis werden anstelle von ResultFunction und Function aussagekräftigere Namen benötigt . Wenn möglich würde ich auch die Parameterlisten als angeben
void
.quelle
Ich fand die von Bruce Eckel beschriebene Methode hilfreich und leicht zu befolgen:
Entnommen aus: Thinking in C ++, Band 1, zweite Ausgabe, Kapitel 3, Abschnitt "Funktionsadressen" von Bruce Eckel.
quelle
Außer natürlich in Klammern geändert. Beachten Sie, dass die Syntax zum Deklarieren dieser Werte genau die Syntax zum Verwenden dieser Variablen zum Abrufen einer Instanz der Basisklasse widerspiegelt.
Im Ernst, das ist nicht schwer auf einen Blick zu lernen. Sie müssen nur bereit sein, einige Zeit damit zu verbringen, die Fertigkeit zu üben. Wenn Sie C-Code pflegen oder anpassen möchten, der von anderen Personen geschrieben wurde, lohnt es sich auf jeden Fall , diese Zeit zu investieren. Es ist auch ein lustiger Partytrick, um andere Programmierer auszuflippen, die es nicht gelernt haben.
Für Ihren eigenen Code: wie immer, die Tatsache , dass etwas kann als does't Einzeiler geschrieben werden bedeutet es sein sollte, es sei denn , es ein extrem übliches Muster ist , die ein Standard Idiom (wie die String-copy - Schleife) worden sind . Sie und diejenigen, die Ihnen folgen, werden viel glücklicher sein, wenn Sie komplexe Typen aus geschichteten Typedefs und schrittweisen Dereferenzen erstellen, anstatt sich auf Ihre Fähigkeit zu verlassen, diese "auf einen Schlag" zu generieren und zu analysieren. Die Leistung wird genauso gut sein, und die Lesbarkeit und Wartbarkeit des Codes wird erheblich besser sein.
Es könnte schlimmer sein, weißt du? Es gab eine rechtliche PL / I-Erklärung, die mit etwas begann wie:
quelle
IF IF = THEN THEN THEN = ELSE ELSE ELSE = ENDIF ENDIF
und wird analysiert alsif (IF == THEN) then (THEN = ELSE) else (ELSE = ENDIF)
.Ich bin zufällig der ursprüngliche Autor der Spiralregel, die ich vor so vielen Jahren geschrieben habe (als ich viele Haare hatte :) und wurde geehrt, als sie dem cfaq hinzugefügt wurde.
Ich habe die Spiralregel geschrieben, um meinen Schülern und Kollegen das Lesen der C-Erklärungen "in ihrem Kopf" zu erleichtern. Das heißt, ohne Software-Tools wie cdecl.org usw. verwenden zu müssen. Es war nie meine Absicht zu erklären, dass die Spiralregel die kanonische Methode zum Parsen von C-Ausdrücken ist. Ich freue mich jedoch zu sehen, dass die Regel im Laufe der Jahre buchstäblich Tausenden von Studenten und Praktikern der C-Programmierung geholfen hat!
Für die Aufzeichnung,
Es wurde mehrfach "richtig" identifiziert, unter anderem von Linus Torvalds (jemand, den ich sehr respektiere), dass es Situationen gibt, in denen meine Spiralregel "zusammenbricht". Das häufigste Wesen:
Wie von anderen in diesem Thread hervorgehoben, könnte die Regel aktualisiert werden, um zu sagen, dass bei der Begegnung mit Arrays einfach alle Indizes so verwendet werden, als ob sie wie folgt geschrieben wären :
Nach der Spiralregel würde ich nun Folgendes erhalten:
"ar ist ein zweidimensionales 10x10-Array von Zeigern auf char"
Ich hoffe, dass die Spiralregel ihre Nützlichkeit beim Lernen von C fortsetzt!
PS:
Ich liebe das Bild "C ist nicht schwer" :)
quelle
(*(*f[]) ()) ()
Auflösen
void
>>(*(*f[]) ())
() = nichtigResoiving
()
>>(*f[]) ()
) = Rückgabe der Funktion (ungültig)Auflösen
*
>>(*f[])
() = Zeiger auf (Rückgabe der Funktion (void))Auflösen
()
>>f[]
) = Funktionsrückgabe (Zeiger auf (Funktionsrückgabe (void)))Auflösen
*
>>f
[] = Zeiger auf (Funktionsrückgabe (Zeiger auf (Funktionsrückgabe (void))))Auflösen
[ ]
>>quelle