wake-up-neo.com

C ist nicht so schwer: void (* (* f []) ()) ()

Ich habe heute gerade ein Bild gesehen und denke, ich würde Erklärungen schätzen. Also hier ist das Bild:

some c code

Ich fand das verwirrend und fragte mich, ob solche Codes jemals praktikabel sind. Ich googelte das Bild und fand ein anderes Bild in this reddit entry, und hier ist das Bild:

some interesting explanation

Also ist dieses "Spirallesen" etwas Gültiges? Analysieren C-Compiler auf diese Weise?
Es wäre toll, wenn es einfachere Erklärungen für diesen seltsamen Code gäbe.
Können solche Codes überhaupt 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 stelle auch die Verwendung solcher Ausdrücke und die Gültigkeit von Spiralregeln in Frage. In Bezug auf diese sind einige nette Antworten bereits gepostet.

183
Motun

Es gibt eine Regel namens "Rechts-/Spiralregel" , um die Bedeutung einer komplexen Deklaration zu ermitteln.

From c-faq :

Es sind drei einfache Schritte zu befolgen:

  1. Beginnen Sie mit dem unbekannten Element und bewegen Sie sich spiralförmig/im Uhrzeigersinn. beim begegnen ersetzen sie die folgenden elemente durch die entsprechenden englischen aussagen:

    [X] Oder []
    => Array X-Größe von ... oder Array undefinierte Größe von ...

    (type1, type2)
    => Funktion übergibt Typ1 und Typ2 und kehrt zurück ...

    *
    => Zeiger auf ...

  2. Machen Sie dies im Spiral-/Uhrzeigersinn weiter, bis alle Spielmarken bedeckt sind.

  3. Löse immer zuerst alles in Klammern!

Sie können den Link oben für Beispiele überprüfen.

Beachten Sie auch, dass es zu Ihrer Unterstützung auch eine Website mit dem Namen gibt:

http://www.cdecl.org

Sie können eine C-Deklaration eingeben, die die englische Bedeutung angibt. Zum

void (*(*f[])())()

es gibt aus:

deklariere f als Array von Zeigern auf Funktionen, die Zeiger auf Funktionen zurückgeben, die void zurückgeben

EDIT:

Wie in den Kommentaren von Random832 hervorgehoben, behandelt die Spiralregel kein Array von Arrays und führt bei (den meisten) dieser Deklarationen zu einem falschen Ergebnis. Beispielsweise ignoriert die Spiralregel für int **x[1][2]; Die Tatsache, dass [] Vorrang vor * Hat.

Wenn Sie sich vor einem Array von Arrays befinden, können Sie zuerst explizite Klammern hinzufügen, bevor Sie die Spiralregel anwenden. Zum Beispiel: int **x[1][2]; Ist das Gleiche wie int **(x[1][2]); (auch gültiges C), da es von der Spiralregel korrekt gelesen wird, da "x ein Array 1 von Array 2 von Zeiger zu Zeiger ist to int "ist die korrekte englische Deklaration.

Beachten Sie, dass dieses Problem auch in dieser Antwort von James Kanze behandelt wurde (darauf hingewiesen durch Hacks in den Kommentaren).

117
ouah

Die "Spiral" -Regelart fällt aus den folgenden Vorrangregeln heraus:

T *a[]    -- a is an array of pointer to T
T (*a)[]  -- a is a pointer to an array of T
T *f()    -- f is a function returning a pointer to T
T (*f)()  -- f is a pointer to a function returning T

Der tiefgestellte [] und der Funktionsaufruf () haben eine höhere Priorität als der unäre *, daher wird *f() als *(f()) und analysiert *a[] wird analysiert als *(a[]).

Wenn Sie also einen Zeiger auf ein Array oder einen Zeiger auf eine Funktion möchten, müssen Sie den * explizit mit dem Bezeichner gruppieren, wie in (*a)[] oder (*f)().

Dann wird Ihnen klar, dass a und f kompliziertere Ausdrücke als nur Bezeichner sein können. in T (*a)[N] kann a ein einfacher Bezeichner sein oder ein Funktionsaufruf wie (*f())[N] (a -> f() ), 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])()) usw.

Es wäre schön, wenn der Indirektionsoperator * anstelle von unary postfix wäre, was das Lesen von Deklarationen von links nach rechts etwas erleichtert (void f[]*()*(); fließt definitiv besser als void (*(*f[])())()). ), aber es ist nicht.

Wenn Sie auf eine haarige Deklaration wie diese stoßen, suchen Sie zunächst die am weitesten links stehende Kennung, und wenden Sie die obigen Vorrangregeln an, indem Sie sie rekursiv auf alle Funktionsparameter anwenden:

         f              -- f
         f[]            -- is an array
        *f[]            -- of pointers  ([] has higher precedence than *)
       (*f[])()         -- to functions
      *(*f[])()         -- returning pointers
     (*(*f[])())()      -- to functions
void (*(*f[])())();     -- returning void

Die Funktion signal in der Standardbibliothek ist wahrscheinlich das Musterbeispiel für diese Art von Wahnsinn:

       signal                                       -- signal
       signal(                          )           -- is a function with parameters
       signal(    sig,                  )           --    sig
       signal(int sig,                  )           --    which is an int and
       signal(int sig,        func      )           --    func
       signal(int sig,       *func      )           --    which is a pointer
       signal(int sig,      (*func)(int))           --    to a function taking an int                                           
       signal(int sig, void (*func)(int))           --    returning void
      *signal(int sig, void (*func)(int))           -- returning a pointer
     (*signal(int sig, void (*func)(int)))(int)     -- to a function taking an int
void (*signal(int sig, void (*func)(int)))(int);    -- and returning void

An diesem Punkt sagen die meisten Leute "benutze typedefs", was sicherlich eine Option ist:

typedef void outerfunc(void);
typedef outerfunc *innerfunc(void);

innerfunc *f[N];

Aber...

Wie würden Sie f in einem Ausdruck verwenden? Sie wissen, dass es sich um eine Reihe von Zeigern handelt, aber wie verwenden Sie sie, um die richtige Funktion auszuführen? Sie müssen die typedefs durchgehen und die korrekte Syntax herausfinden. Im Gegensatz dazu ist die "nackte" Version ziemlich augenfällig, aber sie sagt Ihnen genau, wie f in einem Ausdruck verwendet wird (nämlich (*(*f[i])())();, vorausgesetzt, keine der beiden Funktionen akzeptiert Argumente).

101
John Bode

In C spiegelt die Deklaration die Verwendung wider - so ist es im Standard definiert. Die Erklärung:

void (*(*f[])())()

Ist eine Behauptung, dass der Ausdruck (*(*f[i])())() erzeugt ein Ergebnis vom Typ void. Was bedeutet:

  • f muss ein Array sein, da Sie es indizieren können:

    f[i]
    
  • Die Elemente von f müssen Zeiger sein, da Sie sie dereferenzieren können:

    *f[i]
    
  • Diese Zeiger müssen Zeiger auf Funktionen sein, die keine Argumente annehmen, da Sie sie aufrufen können:

    (*f[i])()
    
  • Die Ergebnisse dieser Funktionen müssen ebenfalls Zeiger sein, da Sie sie dereferenzieren können:

    *(*f[i])()
    
  • Diese Zeiger müssen auch Zeiger auf Funktionen ohne Argumente sein, da Sie sie aufrufen können:

    (*(*f[i])())()
    
  • Diese Funktionszeiger müssen void zurückgeben

Die „Spiralregel“ ist nur ein Mnemon, der eine andere Art bietet, dasselbe zu verstehen.

55
Jon Purdy

Also ist dieses "Spirallesen" etwas Gültiges?

Das Anwenden einer Spiralregel oder das Verwenden von cdecl sind nicht immer gültig. Beides scheitert teilweise. Spiralregel funktioniert in vielen Fällen, aber es ist nicht universell .

Um komplexe Deklarationen zu entschlüsseln, müssen Sie diese beiden einfachen Regeln beachten:

  • Deklarationen immer von innen nach außen lesen : Beginnen Sie mit der innersten Klammer, falls vorhanden. Suchen Sie den Bezeichner, der deklariert wird, und entschlüsseln Sie die Deklaration von dort aus.

  • Wenn es eine Auswahl gibt, bevorzugen Sie immer [] Und () Gegenüber *: Wenn * Steht vor dem Bezeichner und [] Folgt ihm, der Bezeichner stellt ein Array dar, keinen Zeiger. Wenn * Dem Bezeichner vorangeht und () Ihm folgt, stellt der Bezeichner ebenfalls eine Funktion dar, keinen Zeiger. (Klammern können immer verwendet werden, um die normale Priorität von [] Und () Gegenüber * Zu überschreiben.)

Diese Regel beinhaltet tatsächlich Zickzack von einer Seite des Bezeichners zur anderen.

Nun eine einfache Deklaration entziffern

int *a[10];

Regel anwenden:

int *a[10];      "a is"  
     ^  

int *a[10];      "a is an array"  
      ^^^^ 

int *a[10];      "a is an array of pointers"
    ^

int *a[10];      "a is an array of pointers to `int`".  
^^^      

Entschlüsseln wir die komplexe Deklaration wie

void ( *(*f[]) () ) ();  

durch Anwendung der obigen Regeln:

void ( *(*f[]) () ) ();        "f is"  
          ^  

void ( *(*f[]) () ) ();        "f is an array"  
           ^^ 

void ( *(*f[]) () ) ();        "f is an array of pointers" 
         ^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function"   
               ^^     

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer"
       ^   

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function" 
                    ^^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function returning `void`"  
^^^^

Hier ist ein GIF, das zeigt, wie Sie vorgehen (klicken Sie auf das Bild, um es zu vergrößern):

enter image description here


Die hier genannten Regeln stammen aus dem Buch C Programming A Modern Approach von K.N. KING .

30
haccks

Es ist nur eine "Spirale", weil es in dieser Deklaration nur einen Operator auf jeder Seite in jeder Klammerebene gibt. Die Behauptung, dass Sie "spiralförmig" vorgehen, würde generell bedeuten, dass Sie in der Deklaration int ***foo[][][] Zwischen Arrays und Zeigern wechseln, wenn in Wirklichkeit alle Array-Ebenen vor einer der Zeigerebenen liegen.

12
Random832

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 OK für Compilerautoren). Stattdessen sollten typedefs verwendet werden.

7
SergeyA

Als zufälliges Trivia-Faktoid finden Sie es vielleicht amüsant zu wissen, dass es ein tatsächliches Wort auf Englisch gibt, das beschreibt, wie C-Deklarationen gelesen werden: Boustrophedonisch , dh abwechselnd von rechts nach links mit links nach -Recht.

Referenz: Van der Linden, 1994 - Seite 76

7
asamarin

Die Erklärung

void (*(*f[])())()

ist nur eine obskure Art zu sagen

Function f[]

mit

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

In der Praxis werden aussagekräftigere Namen anstelle von ResultFunction und Function benötigt. Wenn möglich würde ich auch die Parameterlisten als void angeben.

5

In Bezug auf die Nützlichkeit dieses Konstrukts sehen Sie beim Arbeiten mit Shellcode eine Menge:

int (*ret)() = (int(*)())code;
ret();

Dieses Muster ist zwar syntaktisch nicht ganz so kompliziert, kommt aber häufig vor.

Vollständigeres Beispiel in this SO question.

Während die Nützlichkeit im Originalbild fraglich ist (ich würde vorschlagen, dass jeder Produktionscode drastisch vereinfacht werden sollte), gibt es einige syntaktische Konstrukte, die ziemlich häufig auftauchen.

5
Casey

Ich fand die von Bruce Eckel beschriebene Methode hilfreich und leicht zu befolgen:

Definieren eines Funktionszeigers

Um einen Zeiger auf eine Funktion zu definieren, die keine Argumente und keinen Rückgabewert hat, sagen Sie:

void (*funcPtr)();

Wenn Sie eine komplexe Definition wie diese betrachten, ist der beste Weg, sie anzugreifen, in der Mitte zu beginnen und sich herauszuarbeiten. „Beginnend in Die Mitte “bedeutet, mit dem Variablennamen zu beginnen, der funcPtr ist. "Herausarbeiten" bedeutet, nach rechts nach dem nächsten Gegenstand zu suchen (in diesem Fall nichts; die rechte Klammer hält Sie davon ab, kurz zu bleiben), dann nach links zu schauen (ein durch das Sternchen gekennzeichneter Zeiger) und dann nach rechts zu schauen (ein leere Argumentliste, die eine Funktion angibt, die keine Argumente akzeptiert), dann nach links schauen (void, was angibt, dass die Funktion keinen Rückgabewert hat). Diese Bewegung von rechts nach links nach rechts funktioniert mit den meisten Deklarationen.

Um zu überprüfen, "in der Mitte beginnen" ("funcPtr ist ein ..."), gehen Sie nach rechts (nichts da - Sie werden durch die rechte Klammer gestoppt), gehen Sie nach links und finden Sie das '*' (" ... Zeiger auf ein ... ”), gehe nach rechts und finde die leere Argumentliste (" ... Funktion, die keine Argumente annimmt ... "), gehe nach links und finde die Leere (" funcPtr is ") ein Zeiger auf eine Funktion, die keine Argumente annimmt und void zurückgibt ”).

Sie fragen sich vielleicht, warum für * funcPtr Klammern erforderlich sind. Wenn Sie sie nicht verwenden, würde der Compiler sehen:

void *funcPtr();

Sie würden eine Funktion deklarieren (die ein void * zurückgibt), anstatt eine Variable zu definieren. Sie können sich den Compiler so vorstellen, als würden Sie denselben Prozess durchlaufen, wenn er herausfindet, wie eine Deklaration oder Definition aussehen soll. Diese Klammern müssen "anstoßen", damit sie nach links zurückgehen und das "*" finden, anstatt nach rechts zu gehen und die leere Argumentliste zu finden.

Komplizierte Erklärungen und Definitionen

Nebenbei bemerkt, wenn Sie erst einmal herausgefunden haben, wie die C- und C++ - Deklarationssyntax funktioniert, können Sie viel kompliziertere Elemente erstellen. Zum Beispiel:

//: C03:ComplicatedDefinitions.cpp

/* 1. */     void * (*(*fp1)(int))[10];

/* 2. */     float (*(*fp2)(int,int,float))(int);

/* 3. */     typedef double (*(*(*fp3)())[10])();
             fp3 a;

/* 4. */     int (*(*f4())[10])();


int main() {} ///:~ 

Gehen Sie durch jedes und verwenden Sie die Rechts-Links-Richtlinie, um es herauszufinden. Nummer 1 besagt, dass "fp1 ein Zeiger auf eine Funktion ist, die ein ganzzahliges Argument akzeptiert und einen Zeiger auf ein Array von 10 ungültigen Zeigern zurückgibt."

Nummer 2 besagt, dass „fp2 ein Zeiger auf eine Funktion ist, die drei Argumente (int, int und float) akzeptiert und einen Zeiger auf eine Funktion zurückgibt, die benötigt ein ganzzahliges Argument und gibt ein float zurück. “

Wenn Sie viele komplizierte Definitionen erstellen, möchten Sie möglicherweise ein typedef verwenden. Nummer 3 zeigt, wie ein typedef jedes Mal die Eingabe der komplizierten Beschreibung erspart. Es heißt: "Ein fp3 ist ein Zeiger auf eine Funktion, die keine Argumente akzeptiert, und gibt einen Zeiger auf ein Array von 10 Zeigern auf Funktionen zurück, die keine Argumente akzeptieren und Double-Werte zurückgeben." Dann heißt es: "a ist einer dieser fp3-Typen." ist im Allgemeinen nützlich, um komplizierte Beschreibungen aus einfachen zu erstellen.

Nummer 4 ist eine Funktionsdeklaration anstelle einer Variablendefinition. Es heißt "f4 ist eine Funktion, die einen Zeiger auf ein Array von 10 Zeigern auf Funktionen zurückgibt, die Ganzzahlen zurückgeben."

Sie werden selten so komplizierte Erklärungen und Definitionen wie diese benötigen. Wenn Sie jedoch die Übung absolvieren, um herauszufinden, wie sie aussehen, werden Sie nicht einmal ein wenig von den etwas komplizierten Problemen gestört, denen Sie im wirklichen Leben begegnen können.

Entnommen aus: Thinking in C++, Band 1, zweite Ausgabe, Kapitel 3, Abschnitt "Funktionsadressen" von Bruce Eckel.

4
user3496846

Denken Sie an diese Regeln für C-Deklarationen
Und der Vorrang wird niemals in Zweifel gezogen werden:
Beginnen Sie mit dem Suffix, fahren Sie mit dem Präfix fort,
Und lies beide Sätze von innen nach außen.
- ich, Mitte der 1980er Jahre

Außer natürlich in Klammern. Beachten Sie, dass die Syntax für die Deklaration genau der Syntax für die Verwendung dieser Variablen entspricht, um eine Instanz der Basisklasse abzurufen.

Im Ernst, das ist auf einen Blick nicht schwer zu lernen; Sie müssen nur bereit sein, einige Zeit damit zu verbringen, die Fertigkeit zu üben. Wenn Sie C-Code, der von anderen Leuten geschrieben wurde, warten oder anpassen möchten, lohnt es sich auf jeden Fall , diese Zeit zu investieren. Es ist auch ein lustiger Partytrick, um andere Programmierer auszurasten, die es nicht gelernt haben.

Für Ihren eigenen Code: Wie immer bedeutet die Tatsache, dass etwas als Einzeiler geschrieben werden kann , dass es nicht so sein sollte, es sei denn, es ist ein Einzeiler extrem gebräuchliches Muster, das zu einer Standardsprache geworden ist (z. B. die String-Copy-Schleife). Sie und diejenigen, die Ihnen folgen, werden viel glücklicher sein, wenn Sie komplexe Typen aus geschichteten Typedefs und schrittweisen Dereferenzierungen erstellen, anstatt sich auf diese zu verlassen Ihre Fähigkeit, diese "auf einen Streich" zu generieren und zu analysieren. Die Leistung ist genauso gut und die Lesbarkeit und Wartbarkeit des Codes wird erheblich verbessert.

Es könnte schlimmer sein, wissen Sie. Es gab eine legale PL/I-Erklärung, die mit etwas begann:

if if if = then then then = else else else = if then ...
4
keshlam

Ich bin zufällig der ursprüngliche Autor der Spiralregel, die ich vor so vielen Jahren geschrieben habe (als ich viele Haare hatte) und die geehrt wurde, als sie der CFAQ hinzugefügt wurde.

Ich habe die Spiralregel geschrieben, um meinen Schülern und Kollegen das Lesen der C-Erklärungen "im Kopf" zu erleichtern. d.h. ohne Software-Tools wie cdecl.org usw. verwenden zu müssen. Es war nie meine Absicht zu erklären, dass die Spiralregel der kanonische Weg zum Parsen von C-Ausdrücken ist. Ich bin jedoch erfreut zu sehen, dass die Regel im Laufe der Jahre buchstäblich Tausenden von C-Programmierschülern und -Praktikern geholfen hat!

Für die Aufzeichnung,

Es wurde mehrfach "richtig" identifiziert, unter anderem durch Linus Torvalds (jemanden, den ich immens respektiere), dass es Situationen gibt, in denen meine Spiralregel "zusammenbricht". Das häufigste Wesen:

char *ar[10][10];

Wie bereits von anderen in diesem Thread erwähnt, könnte die Regel dahingehend aktualisiert werden, dass, wenn Sie auf Arrays stoßen, einfach alle Indizes verbraucht werden als ob geschrieben wie:

char *(ar[10][10]);

Nun, nach der Spiralregel, würde ich bekommen:

"ar ist eine zweidimensionale 10x10-Anordnung von Zeigern auf char"

Ich hoffe, dass die Spiralregel weiterhin nützlich ist, um C zu lernen!

P .:

Ich liebe das "C ist nicht schwer" Bild :)

3
David Anderson
  • nichtig (*(*f[]) ()) ()

void >> wird aufgelöst

  • (*(*f[]) ()) () = nichtig

Wiederaufleben () >>

  • (* (*f[]) ()) = Funktion kehrt zurück (ungültig)

Auflösen von * >>

  • (*f[]) () = Zeiger auf (Funktion, die zurückgibt (ungültig))

Auflösen von () >>

  • (* f[]) = Funktionsrückgabe (Zeiger auf (Funktionsrückgabe (ungültig))

Auflösen von * >>

  • f [] = Zeiger auf (Funktionsrückgabe (Zeiger auf (Funktionsrückgabe (void))))

Auflösen von [ ] >>

  • f = Array von (Zeiger auf (Funktionsrückgabe (Zeiger auf (Funktionsrückgabe (void))))
3
Shubham