web-dev-qa-db-fra.com

C n'est pas si difficile que ça: vide (* (* f []) ()) ()

Je viens de voir une photo aujourd'hui et je pense que j'apprécierais des explications. Alors voici la photo:

some c code

J'ai trouvé cela déroutant et je me suis demandé si de tels codes étaient jamais pratiques. J'ai googlé l'image et trouvé une autre image dans this entrée reddit, et voici cette image:

some interesting explanation

Donc, cette "lecture en spirale" est quelque chose de valable? Est-ce ainsi que les compilateurs C analysent?
Ce serait formidable s'il existe des explications plus simples pour ce code étrange.
Mis à part tout, ce genre de codes peut-il être utile? Si oui, où et quand?

Il y a ne question à propos de "règle spirale", mais je ne demande pas seulement comment elle est appliquée ou comment les expressions sont lues avec cette règle. Je remets en question l'utilisation de telles expressions et la validité de la règle spirale. A ce propos, quelques bonnes réponses sont déjà postées.

183
Motun

Il existe une règle appelée "règle dans le sens horaire/spirale" pour aider à trouver le sens d'une déclaration complexe.

De c-faq :

Il y a trois étapes simples à suivre:

  1. En commençant par l’élément inconnu, déplacez-vous en spirale/dans le sens des aiguilles d’une montre; lorsque vous rencontrez les éléments suivants, remplacez-les par les déclarations en anglais correspondantes:

    [X] Ou []
    => Taille du tableau X de ... ou taille du tableau non définie de ...

    (type1, type2)
    => fonction passant les types1 et 2 renvoyant ...

    *
    => pointeur (s) sur ...

  2. Continuez à faire cela dans une direction en spirale/dans le sens des aiguilles d'une montre jusqu'à ce que tous les jetons soient recouverts.

  3. Toujours résoudre tout ce qui est entre parenthèses en premier!

Vous pouvez consulter le lien ci-dessus pour des exemples.

Notez également que pour vous aider, il existe également un site Web appelé:

http://www.cdecl.org

Vous pouvez entrer une déclaration C qui donnera son sens anglais. Pour

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

il produit:

déclarer f comme un tableau de pointeur vers une fonction retournant un pointeur vers une fonction retournant void

EDIT:

Comme indiqué dans les commentaires par Random832 , la règle en spirale ne traite pas le tableau de tableaux et aboutira à un résultat erroné dans (la plupart de) ces déclarations. Par exemple, pour int **x[1][2];, La règle en spirale ignore le fait que [] A une priorité plus élevée que *.

Lorsque vous vous trouvez devant un tableau de tableaux, vous pouvez d’abord ajouter des parenthèses explicites avant d’appliquer la règle spirale. Par exemple: int **x[1][2]; Est identique à int **(x[1][2]); (également valide C) en raison de la priorité et la règle spirale la lit alors correctement comme suit: "x est un tableau 1 du tableau 2 de pointeur à pointeur to int "qui est la déclaration anglaise correcte.

Notez que ce problème a également été traité dans ce réponse par James Kanze (signalé par haccks dans les commentaires).

117
ouah

La règle "en spirale" est en quelque sorte hors des règles de priorité suivantes:

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

L'indice [] Et les opérateurs d'appel de fonction () Ont une priorité plus élevée que les unaires *, De sorte que *f() est analysé comme *(f()) et *a[] Est analysé comme suit: *(a[]).

Donc, si vous voulez un pointeur sur un tableau ou une fonction, vous devez explicitement regrouper le * Avec l'identifiant, comme dans (*a)[] Ou (*f)().

Ensuite, vous réalisez que a et f peuvent être des expressions plus compliquées que de simples identifiants; dans T (*a)[N], a pourrait être un identifiant simple ou un appel de fonction du type (*f())[N] (a -> f()) ou un tableau du type (*p[M])[N], (a -> p[M]) ou un tableau de pointeurs sur des fonctions du type (*(*p[M])())[N] (a -> (*p[M])()), etc.

Ce serait bien si l'opérateur d'indirection * Était postfixé au lieu de unaire, ce qui rendrait les déclarations plus faciles à lire de gauche à droite (void f[]*()*(); s'écoule nettement mieux que void (*(*f[])())()), mais ce n'est pas.

Lorsque vous rencontrez une déclaration poilue comme celle-ci, commencez par rechercher l'identificateur le plus à gauche et appliquez les règles de priorité ci-dessus, en les appliquant de manière récursive à tous les paramètres de fonction:

         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 fonction signal de la bibliothèque standard est probablement le spécimen de type pour ce type de folie:

       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 ce stade, la plupart des gens disent "use typedefs", ce qui est certainement une option:

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

innerfunc *f[N];

Mais...

Comment feriez-vous tiliserf dans une expression? Vous savez que c'est un tableau de pointeurs, mais comment l'utilisez-vous pour exécuter la fonction correcte? Vous devez passer en revue les types de caractères et trouver la syntaxe correcte. En revanche, la version "naked" est très jolie, mais elle vous dit exactement comment tiliserf dans une expression (à savoir, (*(*f[i])())();, En supposant qu'aucune fonction ne prend arguments).

101
John Bode

En C, la déclaration reflète l’utilisation, c’est ce qui est défini dans la norme. La déclaration:

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

Est-ce une affirmation que l'expression (*(*f[i])())() produit un résultat de type void. Ce qui signifie:

  • f doit être un tableau, car vous pouvez l'indexer:

    f[i]
    
  • Les éléments de f doivent être des pointeurs, car vous pouvez les déréférencer:

    *f[i]
    
  • Ces pointeurs doivent être des pointeurs vers des fonctions ne prenant aucun argument, puisque vous pouvez les appeler:

    (*f[i])()
    
  • Les résultats de ces fonctions doivent également être des pointeurs, car vous pouvez les déréférencer:

    *(*f[i])()
    
  • Ces pointeurs doivent aussi être des pointeurs sur des fonctions ne prenant aucun argument, puisque vous pouvez les appeler:

    (*(*f[i])())()
    
  • Ces pointeurs de fonction doivent renvoyer void

La "règle de la spirale" est juste un mnémonique qui fournit une manière différente de comprendre la même chose.

55
Jon Purdy

Donc, cette "lecture en spirale" est quelque chose de valable?

L'application d'une règle en spirale ou l'utilisation de cdecl ne sont pas toujours valables. Les deux échouent dans certains cas. La règle en spirale fonctionne dans de nombreux cas, mais ce n'est pas universel .

Pour déchiffrer des déclarations complexes, rappelez-vous ces deux règles simples:

  • Lisez toujours les déclarations de l'intérieur : commencez par la parenthèse la plus proche, le cas échéant. Localisez l'identifiant déclaré et commencez à déchiffrer la déclaration à partir de là.

  • Quand il y a un choix, privilégiez toujours [] et () plus de *: Si * précède l'identifiant et [] le suit, l'identifiant représente un tableau et non un pointeur. De même, si * précède l'identifiant et () le suit, l'identifiant représente une fonction et non un pointeur. (Les parenthèses peuvent toujours être utilisées pour remplacer la priorité normale de [] et () plus de *.)

Cette règle implique en fait de zigzaguer d'un côté à l'autre de l'identifiant.

Maintenant déchiffrer une simple déclaration

int *a[10];

Règle d'application:

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

Décryptons la déclaration complexe comme

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

en appliquant les règles ci-dessus:

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

Voici un GIF montrant comment vous allez (cliquez sur l'image pour l'agrandir):

enter image description here


Les règles mentionnées ici sont extraites du livre Programmation en C: une approche moderne de K.N KING .

30
haccks

Ce n'est qu'une "spirale", car dans cette déclaration, il n'y a qu'un seul opérateur de chaque côté dans chaque niveau de parenthèses. Affirmer que vous procédez "dans une spirale" suggérerait généralement que vous alternez entre des tableaux et des pointeurs dans la déclaration int ***foo[][][] quand, en réalité, tous les niveaux de tableau viennent avant les niveaux de pointeur.

12
Random832

Je doute que de telles constructions puissent être utiles dans la vie réelle. Je les déteste même en tant qu’interview pour les développeurs réguliers (probablement OK pour les rédacteurs de compilateur). typedefs devrait être utilisé à la place.

7
SergeyA

En réalité, il peut être amusant de savoir qu’il existe un mot réel en anglais décrivant la façon dont les déclarations C sont lues: boustrophédoniquement , c’est-à-dire alternant de droite à gauche avec de gauche à gauche -droite.

Référence: Van der Linden, 1994 - Page 76

7
asamarin

La déclaration

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

est juste une façon obscure de dire

Function f[]

avec

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

En pratique, des noms plus descriptifs seront nécessaires au lieu de ResultFunction et Fonction. Si possible, je spécifierais également les listes de paramètres sous la forme void.

5
August Karlstrom

En ce qui concerne l'utilité de ceci, lorsque vous travaillez avec un shellcode, vous voyez beaucoup cette construction:

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

Bien que sa syntaxe ne soit pas aussi compliquée, ce modèle particulier est souvent évoqué.

Exemple plus complet dans this SO question.

Ainsi, bien que l’utilité de l’image originale soit discutable (j’aimerais suggérer que tout code de production devrait être radicalement simplifié), il existe certaines constructions syntaxiques qui sont assez fréquentes.

5
Casey

J'ai trouvé la méthode décrite par Bruce Eckel utile et facile à suivre:

Définir un pointeur de fonction

Pour définir un pointeur sur une fonction sans argument ni valeur de retour, vous dites:

void (*funcPtr)();

Lorsque vous recherchez une définition complexe comme celle-ci, la meilleure façon de l'attaquer est de commencer au milieu et de vous en sortir. le milieu ”signifie à partir du nom de la variable, qui est funcPtr. "Travailler votre chemin" signifie regarder vers la droite pour l'élément le plus proche (rien dans ce cas; la parenthèse droite vous coupe court), puis vers la gauche (un pointeur désigné par l'astérisque), puis vers la droite (un liste d'arguments vide indiquant une fonction ne prenant aucun argument), puis en regardant vers la gauche (void, qui indique que la fonction n'a pas de valeur de retour). Cette motion droite-gauche-droite fonctionne avec la plupart des déclarations.

Pour passer en revue, “commencez au milieu” (“funcPtr is a ...”), allez à droite (rien là-bas - vous êtes arrêté par la parenthèse droite), allez à gauche et trouvez le “*” (“ ... pointeur vers un ... "), allez à droite et trouvez la liste d'arguments vide (" ... fonction qui ne prend pas d'argument ... "), allez à gauche et trouvez le vide (" funcPtr est un pointeur sur une fonction qui ne prend aucun argument et renvoie void ”).

Vous vous demandez peut-être pourquoi * funcPtr nécessite des parenthèses. Si vous ne les utilisiez pas, le compilateur verrait:

void *funcPtr();

Vous déclareriez une fonction (qui renvoie un vide *) plutôt que de définir une variable. Vous pouvez penser que le compilateur suit le même processus que celui utilisé pour déterminer ce que doit être une déclaration ou une définition. Il a besoin de ces parenthèses pour "se cogner contre". Il retourne donc à gauche et trouve le "*" au lieu de continuer à droite et de rechercher la liste d’arguments vide.

Déclarations compliquées et définitions

En passant, une fois que vous avez compris le fonctionnement de la syntaxe de déclaration C et C++, vous pouvez créer des éléments beaucoup plus complexes. Par exemple:

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

Parcourez chacun d'eux et utilisez la ligne directrice droite-gauche pour le comprendre. Le nombre 1 indique que "fp1 est un pointeur sur une fonction qui prend un argument entier et renvoie un pointeur sur un tableau de 10 pointeurs vides."

Le nombre 2 indique que "fp2 est un pointeur sur une fonction qui prend trois arguments (int, int et float) et renvoie un pointeur sur une fonction qui prend un argument entier et retourne un float. "

Si vous créez beaucoup de définitions compliquées, vous pouvez utiliser un typedef. Le numéro 3 montre comment un typedef enregistre chaque fois la description compliquée. Il dit "Un fp3 est un pointeur sur une fonction qui ne prend aucun argument et renvoie un pointeur sur un tableau de 10 pointeurs vers des fonctions qui ne prennent aucun argument et ne donnent pas de doublons". Il indique ensuite "a est l'un de ces types fp3". Typedef est généralement utile pour construire des descriptions compliquées à partir de simples.

Le nombre 4 est une déclaration de fonction au lieu d'une définition de variable. Il dit "f4 est une fonction qui renvoie un pointeur sur un tableau de 10 pointeurs vers des fonctions qui renvoient des entiers."

Vous aurez rarement, sinon jamais, besoin de déclarations et de définitions aussi compliquées que celles-ci. Cependant, si vous décidez de les comprendre, vous ne serez même pas dérangé par les problèmes un peu compliqués que vous pourriez rencontrer dans la vie réelle.

Tiré de: Penser en C++, Volume 1, deuxième édition, chapitre 3, section "Adresses de fonction" de Bruce Eckel.

4
user3496846

Rappelez-vous ces règles pour C déclare
Et la préséance ne sera jamais mise en doute:
Commencez par le suffixe, passez au préfixe,
Et lisez les deux ensembles de l'intérieur vers l'extérieur.
- moi, au milieu des années 1980

Sauf modification entre parenthèses, bien sûr. Et notez que la syntaxe permettant de les déclarer reflète exactement la syntaxe permettant d’utiliser cette variable pour obtenir une instance de la classe de base.

Sérieusement, ce n'est pas difficile à apprendre en un coup d'œil; vous devez juste être prêt à passer du temps à pratiquer cette technique. Si vous souhaitez conserver ou adapter du code C écrit par d’autres personnes, il vaut certainement la peine d’investir ce temps. C'est aussi un tour de passe-passe amusant pour faire peur aux autres programmeurs qui ne l'ont pas appris.

Pour votre propre code: comme toujours, le fait que quelque chose puisse être écrit en une ligne ne signifie pas que cela devrait l'être, à moins que ce soit un modèle extrêmement courant qui est devenu un idiome standard (tel que la boucle de copie de chaîne). Vous, et ceux qui vous suivent, serez beaucoup plus heureux si vous créez des types complexes à partir de types de couches et de déréférences pas à pas, plutôt que de vous appuyer sur votre capacité à générer et à analyser ces "un seul swop". Les performances seront tout aussi bonnes, et la lisibilité et la maintenabilité du code seront énormément meilleures.

Cela pourrait être pire, vous savez. Il y avait une déclaration légale PL/I qui commençait par quelque chose comme:

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

Je suis l’auteur de la règle spirale que j’ai écrite il ya tant d’années (quand j’avais beaucoup de cheveux :) et que j’étais honorée lorsqu’elle a été ajoutée au cfaq.

J'ai écrit la règle en spirale afin de faciliter la lecture des déclarations en C par mes étudiants et mes collègues "dans leur tête"; c'est-à-dire sans avoir à utiliser d'outils logiciels tels que cdecl.org, etc. Je n'ai jamais eu l'intention de déclarer que la règle spiral était le moyen canonique d'analyser les expressions en C. Je suis cependant ravi de constater que cette règle a aidé littéralement des milliers d'étudiants en programmation C et de praticiens au fil des ans!

Pour le compte rendu,

Il a été "correctement" identifié à de nombreuses reprises sur de nombreux sites, y compris par Linus Torvalds (une personne que je respecte immensément), qu'il y avait des situations où ma règle en spirale "s'effondrait". Le plus commun étant:

char *ar[10][10];

Comme l'ont souligné d'autres personnes dans ce fil de discussion, la règle pourrait être mise à jour pour indiquer que lorsque vous rencontrez des tableaux, utilisez simplement tous les index comme si écrits comme suit:

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

Maintenant, suivant la règle de la spirale, je voudrais obtenir:

"ar est un tableau bidimensionnel 10x10 de pointeurs sur char"

J'espère que la règle en spirale continuera à être utile pour apprendre le langage C!

P.S .:

J'aime l'image "C n'est pas difficile" :)

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

Résolution void >>

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

Resoiving () >>

  • (* (*f[]) ()) = fonction retournant (vide)

Résolution * >>

  • (*f[]) () = pointeur sur (fonction retournant (void))

Résolution () >>

  • (* f[]) = fonction retournant (pointeur sur (fonction retournant (void)))

Résolution * >>

  • f [] = pointeur sur (fonction retournant (pointeur sur (fonction retournant (void))))

Résolution [ ] >>

  • f = tableau de (pointeur vers (fonction retournant (pointeur vers (fonction retournant (void))))))
3
Shubham