C non è così difficile: void (* (* f []) ()) ()


188

Ho appena visto una foto oggi e penso che apprezzerei le spiegazioni. Quindi ecco l'immagine:

qualche codice c

Ho trovato questo confuso e mi chiedevo se tali codici fossero mai pratici. Ho cercato su Google l'immagine e ho trovato un'altra immagine in questa voce reddit, ed ecco quella foto:

qualche spiegazione interessante

Quindi questa "lettura a spirale" è qualcosa di valido? È così che i compilatori C analizzano?
Sarebbe bello se ci fossero spiegazioni più semplici per questo strano codice.
A parte tutto, questo tipo di codici può essere utile? In tal caso, dove e quando?

C'è una domanda sulla "regola della spirale", ma non sto solo chiedendo come viene applicata o come le espressioni vengono lette con quella regola. Sto mettendo in discussione l'uso di tali espressioni e anche la validità della regola a spirale. Riguardo a questi, alcune belle risposte sono già state pubblicate.


9
In che modo la spiegazione potrebbe essere più semplice? Copre tutti gli aspetti della definizione di fw / un paio di parole per ciascun punto chiave.
Scott Hunter,

30
Forse C è difficile? Che in realtà dichiara fcome una matrice di puntatori a funzioni che potrebbero accettare qualsiasi argomento. Se fosse void (*(*f[])(void))(void);, allora sì, sarebbero funzioni che non accettano argomenti ...
txtechhelp

18
In pratica, non codificare un codice così oscuro. Usa typedef per le firme
Basile Starynkevitch il

4
qualsiasi dichiarazione relativa a puntatori a funzioni può essere difficile. Ciò non significa che il normale C o C ++ sia difficile allo stesso modo. Altre lingue risolvono questo problema in diversi modi, incluso il mancato puntatore di funzione, che in alcuni casi può essere un'omissione significativa
Kate Gregory,

20
Se socchiudi gli occhi, sembra LISP.
user2023861

Risposte:


117

Esiste una regola chiamata "Regola in senso orario / a spirale" per aiutare a trovare il significato di una dichiarazione complessa.

Da c-faq :

Ci sono tre semplici passaggi da seguire:

  1. Partendo dall'elemento sconosciuto, muoviti in una direzione a spirale / in senso orario; durante l'ecounter i seguenti elementi li sostituiscono con le corrispondenti dichiarazioni inglesi:

    [X]oppure []
    => Dimensione array X di ... o Dimensione array non definita di ...

    (type1, type2)
    => funzione che passa type1 e type2 restituendo ...

    *
    => puntatore / i a ...

  2. Continua a farlo in una direzione a spirale / in senso orario fino a quando tutti i token non sono stati coperti.

  3. Risolvi sempre tutto tra parentesi!

Puoi controllare il link sopra per esempi.

Nota anche che per aiutarti c'è anche un sito web chiamato:

http://www.cdecl.org

È possibile inserire una dichiarazione C e darà il suo significato inglese. Per

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

produce:

dichiarare f come matrice di puntatore alla funzione restituendo il puntatore alla funzione restituendo il vuoto

MODIFICARE:

Come sottolineato nei commenti di Random832 , la regola a spirale non si occupa di array di matrici e porterà a un risultato errato in (la maggior parte) di tali dichiarazioni. Ad esempio, per int **x[1][2];la regola della spirale si ignora il fatto che []ha una precedenza superiore *.

Di fronte all'array di array, è possibile aggiungere parentesi esplicite prima di applicare la regola a spirale. Ad esempio: int **x[1][2];è lo stesso di int **(x[1][2]);(anche valido C) per precedenza e la regola della spirale la legge correttamente come "x è un array 1 di array 2 da puntatore a puntatore a int" che è la dichiarazione inglese corretta.

Si noti che questo problema è stato trattato anche in questa risposta da James Kanze (sottolineato da Haccks nei commenti).


5
Vorrei che cdecl.org fosse migliore
Grady Player il

8
Non esiste una "regola a spirale" ... "int *** foo [] [] []" definisce una matrice di matrici di matrici di puntatori a puntatori a puntatori. La "spirale" deriva solo dal fatto che questa dichiarazione è avvenuta per raggruppare le cose tra parentesi in un modo che le faceva alternare. È tutto a destra, quindi a sinistra, all'interno di ciascuna serie di parentesi.
Casuale 832

1
@ Random832 Esiste una "regola a spirale", che copre il caso che hai appena citato, ovvero parla di come gestire parentesi / matrici ecc. Naturalmente non è una regola C standard, ma un buon mnemonico per capire come trattare con dichiarazioni complicate. IMHO, è estremamente utile e ti salva quando sei nei guai o quando cdecl.org non può analizzare la dichiarazione. Naturalmente non bisogna abusare di tali dichiarazioni, ma è bene sapere come vengono analizzate.
vsoftco

5
@vsoftco Ma non "si muove in una spirale / in senso orario" se ti giri solo quando raggiungi le parentesi.
Casuale 832

2
ouah, dovresti menzionare che la regola della spirale non è universale .
Hawcks

105

La regola "spirale" non rientra nelle seguenti regole di precedenza:

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

Gli operatori di sottoscrizione []e chiamata di funzione ()hanno una precedenza maggiore rispetto all'unario *, quindi *f()viene analizzato come *(f())e *a[]viene analizzato come *(a[]).

Quindi, se si desidera un puntatore a un array o un puntatore a una funzione, è necessario raggruppare esplicitamente il carattere *con l'identificatore, come in (*a)[]o (*f)().

Quindi te ne rendi conto ae fpossono essere espressioni più complicate di semplici identificatori; in T (*a)[N], apotrebbe essere un semplice identificatore, oppure potrebbe essere una chiamata di funzione come (*f())[N]( a-> f()), oppure potrebbe essere una matrice come (*p[M])[N], ( a-> p[M]), oppure potrebbe essere una matrice di puntatori a funzioni come (*(*p[M])())[N]( a-> (*p[M])()), eccetera.

Sarebbe bello se l'operatore di riferimento indiretto *fosse postfisso anziché unario, il che renderebbe le dichiarazioni un po 'più facili da leggere da sinistra a destra ( void f[]*()*();scorre sicuramente meglio di void (*(*f[])())()), ma non lo è.

Quando ti imbatti in una dichiarazione pelosa come quella, inizia trovando l' identificatore più a sinistra e applica le regole di precedenza sopra, applicandole in modo ricorsivo a qualsiasi parametro di funzione:

         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

La signalfunzione nella libreria standard è probabilmente il tipo di campione per questo tipo di follia:

       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

A questo punto la maggior parte delle persone dice "usa typedefs", che è certamente un'opzione:

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

innerfunc *f[N];

Ma...

Come si usa f in un'espressione? Sai che è un array di puntatori, ma come lo usi per eseguire la funzione corretta? Devi andare oltre i typedef e decifrare la sintassi corretta. Al contrario, la versione "nuda" è piuttosto accattivante, ma ti dice esattamente come usare f in un'espressione (vale a dire (*(*f[i])())();, supponendo che nessuna delle funzioni accetti argomenti).


7
Grazie per aver dato l'esempio di "segnale", dimostrando che questo genere di cose appare allo stato selvatico.
Justsalt,

Questo è un ottimo esempio.
Casey,

Mi è piaciuto il tuo falbero di decelerazione, spiegando la precedenza ... per qualche ragione mi viene sempre un calcio fuori dall'arte ASCII, specialmente quando si tratta di spiegare le cose :)
txtechhelp

1
supponendo che nessuna delle due funzioni accetti argomenti : allora devi usare voidtra parentesi le funzioni, altrimenti può prendere qualsiasi argomento.
Hawcks

1
@haccks: per la dichiarazione, sì; Stavo parlando della chiamata di funzione.
John Bode,

57

In C, la dichiarazione rispecchia l'utilizzo, ecco come è definita nella norma. La dichiarazione:

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

È un'asserzione che l'espressione (*(*f[i])())()produce un risultato di tipo void. Che significa:

  • f deve essere un array, poiché è possibile indicizzarlo:

    f[i]
  • Gli elementi di fdevono essere puntatori, poiché è possibile dereferenziarli:

    *f[i]
  • Tali puntatori devono essere puntatori a funzioni che non accettano argomenti, poiché è possibile chiamarli:

    (*f[i])()
  • I risultati di tali funzioni devono anche essere puntatori, dal momento che è possibile dereferenziarli:

    *(*f[i])()
  • Questi puntatori devono anche essere puntatori a funzioni che non accettano argomenti, poiché è possibile chiamarli:

    (*(*f[i])())()
  • Quei puntatori a funzione devono tornare void

La "regola a spirale" è solo un mnemonico che fornisce un modo diverso di comprendere la stessa cosa.


3
Ottimo modo di vederlo che non avevo mai visto prima. +1
martedì

4
Bello. Visto in questo modo, è davvero semplice . In realtà un po 'più facile di qualcosa di simile vector< function<function<void()>()>* > f, specialmente se si aggiunge in std::s. (Ma bene, l'esempio è inventato ... f :: [IORef (IO (IO ()))]sembra anche strano.)
circa il

1
@TimoDenk: la dichiarazione a[x]indica che l'espressione a[i]è valida quando i >= 0 && i < x. Considerando che a[]lascia la dimensione non specificata ed è quindi identico a *a: indica che l'espressione a[i](o equivalentemente *(a + i)) è valida per un certo intervallo di i.
Jon Purdy,

4
Questo è di gran lunga il modo più semplice di pensare ai tipi C, grazie per questo
Alex Ozer,

4
Amo questo! Molto più facile da ragionare rispetto alle spirali sciocche. (*f[])()è un tipo che puoi indicizzare, quindi dereference, quindi chiamare, quindi è un array di puntatori a funzioni.
Lynn,

32

Quindi questa "lettura a spirale" è qualcosa di valido?

L'applicazione della regola a spirale o l'utilizzo del cdecl non sono sempre validi. Entrambi falliscono in alcuni casi. La regola a spirale funziona in molti casi, ma non è universale .

Per decifrare dichiarazioni complesse, ricorda queste due semplici regole:

  • Leggi sempre le dichiarazioni dall'interno verso l'esterno : inizia dalla parentesi più interna, se presente. Individua l'identificatore che viene dichiarato e inizia a decifrare la dichiarazione da lì.

  • Quando c'è una scelta, preferisci sempre []e ()oltre* : se *precede l'identificatore e lo []segue, l'identificatore rappresenta un array, non un puntatore. Allo stesso modo, se *precede l'identificatore e lo ()segue, l'identificatore rappresenta una funzione, non un puntatore. (Le parentesi possono sempre essere utilizzate per ignorare la normale priorità di []e ()oltre *.)

Questa regola comporta in realtà lo zigzag da un lato dell'identificatore all'altro.

Ora decifrando una semplice dichiarazione

int *a[10];

Regola di applicazione:

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`".  
^^^      

Decifriamo la dichiarazione complessa come

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

applicando le regole di cui sopra:

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`"  
^^^^

Ecco una GIF che dimostra come andare (fare clic sull'immagine per ingrandirla):

inserisci qui la descrizione dell'immagine


Le regole qui menzionate sono tratte dal libro C Programming A Modern Approach di KN KING .


Questo è proprio come l'approccio della norma, ovvero "utilizzo dei mirror delle dichiarazioni". Vorrei chiedere qualcos'altro a questo punto: suggerisci il libro di KN King? Sto vedendo molte belle recensioni sul libro.
Motun,

1
Si. Suggerisco quel libro. Ho iniziato a programmare da quel libro. Buoni testi e problemi lì dentro.
Hawcks

Potete fornire un esempio di mancata comprensione di una dichiarazione da parte di cdecl? Ho pensato che cdecl usasse le stesse regole di analisi dei compilatori e, per quanto ne so, funziona sempre.
Fabio dice di reintegrare Monica il

@FabioTurati; Una funzione non può restituire matrici o funzioni. char (x())[5]dovrebbe provocare un errore di sintassi ma, cdecl lo analizza come: dichiara xcome funzione che restituisce la matrice 5 dichar .
Hawcks

12

È solo una "spirale" perché in questa dichiarazione vi è solo un operatore su ciascun lato all'interno di ogni livello tra parentesi. Affermare che si procede "in una spirale" in genere suggerirebbe di alternare matrici e puntatori nella dichiarazione int ***foo[][][]quando in realtà tutti i livelli di matrice precedono uno qualsiasi dei livelli di puntatore.


Bene, nell '"approccio a spirale", vai il più a destra possibile, quindi il più a sinistra che puoi, ecc. Ma viene spesso spiegato erroneamente ...
Lynn,

7

Dubito che costruzioni come questa possano essere utili nella vita reale. Li detesto persino come domande di intervista per gli sviluppatori regolari (probabilmente OK per gli autori di compilatori). si dovrebbe usare invece typedefs.


3
Tuttavia, è importante sapere come analizzarlo, anche solo per sapere come analizzare il typedef!
inetknght,

1
@inetknght, il modo in cui lo fai con typedefs è di averli abbastanza semplici da non richiedere analisi.
SergeyA

2
Le persone che fanno questo tipo di domande durante le interviste lo fanno solo per accarezzare il loro ego.
Casey,

1
@JohnBode, e ti faresti un favore digitando il valore restituito della funzione.
Sergey,

1
@JohnBode, trovo che sia una questione di scelta personale non degna di discussione. Vedo la tua preferenza, ho ancora la mia.
Sergey,

7

Come curiosità casuale, potresti trovare divertente sapere che esiste una parola in inglese per descrivere come vengono lette le dichiarazioni C: Boustrophedonically , cioè alternando da destra a sinistra con da sinistra a destra.

Riferimento: Van der Linden, 1994 - Pagina 76


1
Quella parola non indica dentro come nidificato da parentesi o su una sola riga. Descrive un modello "serpente", con una linea LTR seguita da una linea RTL.
Potatoswatter il

5

Per quanto riguarda l'utilità di questo, quando si lavora con shellcode si vede molto questo costrutto:

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

Sebbene non sia così sintatticamente complicato, questo particolare schema emerge molto.

Esempio più completo in questa domanda SO.

Quindi, sebbene l'utilità nella misura originale sia discutibile (suggerirei che qualsiasi codice di produzione dovrebbe essere drasticamente semplificato), ci sono alcuni costrutti sintattici che emergono abbastanza.


5

La dichiarazione

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

è solo un modo oscuro di dire

Function f[]

con

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

In pratica, saranno necessari nomi più descrittivi invece di ResultFunction e Function . Se possibile, vorrei anche specificare gli elenchi di parametri come void.


4

Ho trovato il metodo descritto da Bruce Eckel per essere utile e facile da seguire:

Definizione di un puntatore a funzione

Per definire un puntatore a una funzione che non ha argomenti e nessun valore di ritorno, dici:

void (*funcPtr)();

Quando stai guardando una definizione complessa come questa, il modo migliore per attaccarla è iniziare nel mezzo e uscire. "Avvio nel mezzo" significa iniziare dal nome della variabile, che è funcPtr. "Lavorare per uscire" significa guardare a destra per l'elemento più vicino (niente in questo caso; la parentesi destra ti blocca brevemente), quindi guardare a sinistra (un puntatore indicato dall'asterisco), quindi guardare a destra (un elenco di argomenti vuoto che indica una funzione che non accetta argomenti), quindi cerca a sinistra (vuoto, che indica che la funzione non ha valore restituito). Questo movimento destra-sinistra-destra funziona con la maggior parte delle dichiarazioni.

Per rivedere, "inizia nel mezzo" ("funcPtr è un ..."), vai a destra (niente lì - sei fermato dalla parentesi destra), vai a sinistra e trova il '*' (" ... puntatore a ... "), vai a destra e trova la lista di argomenti vuota (" ... funzione che non accetta argomenti ... "), vai a sinistra e trova il vuoto (" funcPtr è un puntatore a una funzione che non accetta argomenti e restituisce void ”).

Potresti chiederti perché * funcPtr richiede parentesi. Se non li avessi usati, il compilatore avrebbe visualizzato:

void *funcPtr();

Dichiareresti una funzione (che restituisce un vuoto *) anziché definire una variabile. Puoi pensare al compilatore come se stesse attraversando lo stesso processo che fai quando capisce cosa dovrebbe essere una dichiarazione o una definizione. Ha bisogno che quelle parentesi si "scontrino", quindi torna a sinistra e trova il "*", invece di continuare a destra e trovare un elenco di argomenti vuoto.

Dichiarazioni e definizioni complicate

A parte questo, una volta capito come funziona la sintassi della dichiarazione C e C ++ puoi creare elementi molto più complicati. Per esempio:

//: 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() {} ///:~ 

Cammina attraverso ognuno di essi e usa la linea guida destra-sinistra per capirlo. Il numero 1 dice "fp1 è un puntatore a una funzione che accetta un argomento intero e restituisce un puntatore a una matrice di 10 puntatori vuoti".

Il numero 2 dice "fp2 è un puntatore a una funzione che accetta tre argomenti (int, int e float) e restituisce un puntatore a una funzione che accetta un argomento intero e restituisce un float".

Se stai creando molte definizioni complicate, potresti voler usare un typedef. Il numero 3 mostra come un typedef salva digitando la descrizione complicata ogni volta. Dice "Un fp3 è un puntatore a una funzione che non accetta argomenti e restituisce un puntatore a un array di 10 puntatori a funzioni che non accettano argomenti e restituiscono double". Quindi dice "a è uno di questi tipi di mp3". typedef è generalmente utile per creare descrizioni complicate da quelle semplici.

Il numero 4 è una dichiarazione di funzione anziché una definizione variabile. Dice "f4 è una funzione che restituisce un puntatore a un array di 10 puntatori a funzioni che restituiscono numeri interi".

Raramente se mai avrai bisogno di dichiarazioni e definizioni così complicate come queste. Tuttavia, se attraversi l'esercizio di capirli, non sarai nemmeno leggermente disturbato da quelli leggermente complicati che potresti incontrare nella vita reale.

Tratto da: Thinking in C ++ Volume 1, seconda edizione, capitolo 3, sezione "Indirizzi di funzione" di Bruce Eckel.


4

Ricorda queste regole per C dichiara
e la precedenza non sarà mai in dubbio:
inizia con il suffisso, procedi con il prefisso
e leggi entrambi i set dall'interno, fuori.
- io, metà degli anni '80

Salvo quanto modificato tra parentesi, ovviamente. E nota che la sintassi per dichiarare questi rispecchia esattamente la sintassi per usare quella variabile per ottenere un'istanza della classe base.

Seriamente, questo non è difficile da imparare a colpo d'occhio; devi solo essere disposto a passare un po 'di tempo a praticare l'abilità. Se hai intenzione di mantenere o adattare il codice C scritto da altre persone, vale sicuramente la pena investire quel tempo. È anche un divertente trucco da festa per impazzire altri programmatori che non l'hanno imparato.

Per il tuo codice: come sempre, il fatto che qualcosa possa essere scritto come una riga non significa che dovrebbe essere, a meno che non sia un modello estremamente comune che è diventato un linguaggio standard (come il ciclo stringa-copia) . Tu e quelli che ti seguono, sarai molto più felice se costruisci tipi complessi a partire da typedef stratificati e dereferenze passo-passo invece di fare affidamento sulla tua capacità di generare e analizzare questi "in un colpo solo". Le prestazioni saranno altrettanto buone e la leggibilità e la manutenibilità del codice saranno notevolmente migliori.

Potrebbe andare peggio, lo sai. C'è stata una dichiarazione PL / I legale che è iniziata con qualcosa del tipo:

if if if = then then then = else else else = if then ...

2
L'istruzione PL / I è stata IF IF = THEN THEN THEN = ELSE ELSE ELSE = ENDIF ENDIFed è analizzata come if (IF == THEN) then (THEN = ELSE) else (ELSE = ENDIF).
Cole Johnson,

Penso che ci fosse una versione che ha fatto un ulteriore passo in avanti usando un'espressione condizionale IF / THEN / ELSE (equivalente a C's? :), che ha ottenuto il terzo set nel mix ... ma sono passati alcuni decenni e potrebbe aver dipendeva da un dialetto particolare della lingua. Resta il punto che qualsiasi lingua ha almeno una forma patologica.
Keshlam,

4

Mi capita di essere l'autore originale della regola della spirale che ho scritto oh così tanti anni fa (quando avevo molti capelli :) ed è stato onorato quando è stato aggiunto al cfaq.

Ho scritto la regola della spirale come un modo per rendere più facile per i miei studenti e colleghi leggere le dichiarazioni C "nella loro testa"; cioè, senza dover usare strumenti software come cdecl.org, ecc. Non è mai stato mia intenzione dichiarare che la regola a spirale sia il modo canonico per analizzare le espressioni C. Sono comunque felice di vedere che la regola ha aiutato letteralmente migliaia di studenti e professionisti in programmazione C nel corso degli anni!

Per il record,

È stato "correttamente" identificato più volte in molti siti, tra cui Linus Torvalds (qualcuno che rispetto immensamente), che ci sono situazioni in cui la mia regola a spirale "si infrange". L'essere più comune:

char *ar[10][10];

Come sottolineato da altri in questo thread, la regola potrebbe essere aggiornata per dire che quando si incontrano array, è sufficiente consumare tutti gli indici come se fossero scritti come:

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

Ora, seguendo la regola della spirale, otterrei:

"ar è un array bidimensionale 10x10 di puntatori al carattere"

Spero che la regola della spirale continui a essere utile nell'apprendimento del C!

PS:

Adoro l'immagine "C non è difficile" :)


3
  • vuoto (*(*f[]) ()) ()

Risoluzione void>>

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

Resoiving ()>>

  • (* (*f[]) ()) = funzione di ritorno (nulla)

Risoluzione *>>

  • (*f[]) () = puntatore a (funzione return (void))

Risoluzione ()>>

  • (* f[]) = funzione di ritorno (puntatore a (funzione di ritorno (vuoto)))

Risoluzione *>>

  • f[] = puntatore a (funzione ritorno (puntatore a (funzione ritorno (vuoto))))

Risoluzione [ ]>>

  • f = array di (puntatore a (funzione ritorno (puntatore a (funzione ritorno (vuoto)))))
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.