272 votes

Compréhension des typedefs pour les pointeurs de fonction en C

J'ai toujours été un peu perplexe lorsque je lisais le code des autres qui avait des typedef pour des pointeurs de fonctions avec des arguments. Je me rappelle que cela m'a pris un certain temps pour comprendre une définition similaire tout en essayant de comprendre un algorithme numérique écrit en C il y a quelque temps. Pourriez-vous partager vos conseils et réflexions sur la manière d'écrire de bons typedef pour les pointeurs de fonctions (ce qu'il faut faire et ce qu'il ne faut pas faire), pourquoi ils sont utiles et comment comprendre le travail des autres? Merci!

1 votes

Pouvez-vous fournir quelques exemples ?

2 votes

Est-ce que vous ne voulez pas dire des typedefs pour les pointeurs de fonction, au lieu de macros pour les pointeurs de fonction? J'ai vu le premier mais pas le dernier.

0 votes

339voto

Jonathan Leffler Points 299946

Considérez le signal() de la norme C :

extern void (*signal(int, void(*)(int)))(int);

C'est une fonction qui prend deux arguments, un nombre entier et un pointeur vers une fonction qui prend un nombre entier comme argument et ne renvoie rien, et c'est ( signal() ) renvoie un pointeur vers une fonction qui prend un entier comme argument et ne renvoie rien.

Si vous écrivez :

typedef void (*SignalHandler)(int signum);

alors vous pouvez déclarer à la place signal() comme :

extern  SignalHandler signal(int signum, SignalHandler handler);

Cela signifie la même chose, mais est généralement considéré comme un peu plus facile à lire. Il est plus clair que la fonction prend un int et un SignalHandler et renvoie un SignalHandler .

Il faut cependant un peu de temps pour s'y habituer. La seule chose que vous ne pouvez pas faire, cependant, est d'écrire une fonction de gestion de signal en utilisant la fonction SignalHandler typedef dans la définition de la fonction.

Je suis toujours de la vieille école qui préfère invoquer un pointeur de fonction comme :

(*functionpointer)(arg1, arg2, ...);

La syntaxe moderne utilise juste :

functionpointer(arg1, arg2, ...);

Je peux comprendre pourquoi cela fonctionne - je préfère simplement savoir que je dois chercher l'endroit où la variable est initialisée plutôt que de chercher une fonction appelée functionpointer .


a commenté Sam :

J'ai déjà vu cette explication auparavant. Et puis, comme c'est le cas maintenant, je pense que ce que je n'ai pas compris, c'est le lien entre les deux déclarations :

    extern void (*signal(int, void()(int)))(int);  /*and*/

    typedef void (*SignalHandler)(int signum);
    extern SignalHandler signal(int signum, SignalHandler handler);

Ou, ce que je veux demander c'est, quel est le concept sous-jacent que l'on peut utiliser pour arriver à la deuxième version que vous avez ? Quel est le fondamental qui relie "SignalHandler" et le premier typedef ? Je pense que ce qui doit être expliqué ici est ce que le typedef fait réellement ici.

Essayons à nouveau. La première est tirée directement de la norme C - je l'ai retapée, et j'ai vérifié que j'avais bien mis les parenthèses (pas avant de l'avoir corrigée - c'est difficile à retenir).

Tout d'abord, rappelez-vous que typedef introduit un alias pour un type. Ainsi, l'alias est SignalHandler et son type est :

un pointeur vers une fonction qui prend un entier comme argument et ne renvoie rien.

La partie "ne renvoie rien" s'écrit void ; l'argument qui est un entier est (je crois) auto-explicatif. La notation suivante est simplement (ou non) la façon dont le C épelle pointeur à la fonction prenant les arguments comme spécifié et retournant le type donné :

type (*function)(argtypes);

Après avoir créé le type de gestionnaire de signaux, je peux l'utiliser pour déclarer des variables, etc. Par exemple :

static void alarm_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d)\n", __func__, signum);
}

static void signal_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d) - exiting\n", __func__, signum);
    exit(1);
}

static struct Handlers
{
    int              signum;
    SignalHandler    handler;
} handler[] =
{
    { SIGALRM,   alarm_catcher  },
    { SIGINT,    signal_catcher },
    { SIGQUIT,   signal_catcher },
};

int main(void)
{
    size_t num_handlers = sizeof(handler) / sizeof(handler[0]);
    size_t i;

    for (i = 0; i < num_handlers; i++)
    {
        SignalHandler old_handler = signal(handler[i].signum, SIG_IGN);
        if (old_handler != SIG_IGN)
            old_handler = signal(handler[i].signum, handler[i].handler);
        assert(old_handler == SIG_IGN);
    }

    ...continue with ordinary processing...

    return(EXIT_SUCCESS);
}

Alors, qu'avons-nous fait ici - à part omettre 4 en-têtes standard qui seraient nécessaires pour que le code se compile proprement ?

Les deux premières fonctions sont des fonctions qui prennent un seul entier et ne renvoient rien. L'une d'entre elles ne renvoie rien du tout grâce à la fonction exit(1); mais l'autre revient après avoir imprimé un message. Sachez que la norme C ne vous permet pas de faire grand-chose à l'intérieur d'un gestionnaire de signaux ; POSIX est un peu plus généreux dans ce qui est autorisé, mais ne sanctionne pas officiellement le fait d'appeler fprintf() . J'imprime également le numéro du signal qui a été reçu. Dans le alarm_handler() la valeur sera toujours SIGALRM car c'est le seul signal pour lequel il est un gestionnaire, mais signal_handler() pourrait obtenir SIGINT o SIGQUIT comme le numéro du signal car la même fonction est utilisée pour les deux.

Ensuite, je crée un tableau de structures, où chaque élément identifie un numéro de signal et le gestionnaire à installer pour ce signal. J'ai choisi de me préoccuper de 3 signaux ; je me préoccuperais souvent de SIGHUP , SIGPIPE y SIGTERM et de savoir s'ils sont définis ( #ifdef compilation conditionnelle), mais cela ne fait que compliquer les choses. J'utiliserais aussi probablement POSIX sigaction() au lieu de signal() mais c'est une autre question ; restons-en à ce que nous avons commencé.

El main() La fonction itère sur la liste des gestionnaires à installer. Pour chaque gestionnaire, elle appelle d'abord signal() pour savoir si le processus est actuellement en train d'ignorer le signal, et pendant ce temps, installe SIG_IGN comme gestionnaire, ce qui garantit que le signal reste ignoré. Si le signal n'a pas été ignoré auparavant, il appelle alors signal() à nouveau, cette fois pour installer le gestionnaire de signaux préféré. (L'autre valeur est vraisemblablement SIG_DFL le gestionnaire de signal par défaut pour le signal). Puisque le premier appel à 'signal()' a défini le gestionnaire à SIG_IGN y signal() retourne le gestionnaire d'erreur précédent, la valeur de old après le if La déclaration doit être SIG_IGN - d'où l'affirmation. (Eh bien, cela pourrait être SIG_ERR si quelque chose allait dramatiquement mal - mais alors je l'apprendrais par le tir d'assertion).

Le programme fait ensuite son travail et sort normalement.

Notez que le nom d'une fonction peut être considéré comme un pointeur vers une fonction du type approprié. Lorsque vous n'appliquez pas les parenthèses d'appel de fonction - comme dans les initialisateurs, par exemple - le nom de la fonction devient un pointeur de fonction. C'est également pour cette raison qu'il est raisonnable d'invoquer des fonctions via la fonction pointertofunction(arg1, arg2) la notation ; lorsque vous voyez alarm_handler(1) vous pouvez considérer que alarm_handler est un pointeur vers la fonction et donc alarm_handler(1) est une invocation d'une fonction via un pointeur de fonction.

Donc, jusqu'à présent, j'ai montré qu'une SignalHandler est relativement simple à utiliser, tant que vous avez le bon type de valeur à lui attribuer - ce que les deux fonctions de gestion des signaux fournissent.

Maintenant, nous revenons à la question : comment les deux déclarations de signal() les uns par rapport aux autres.

Passons en revue la deuxième déclaration :

 extern SignalHandler signal(int signum, SignalHandler handler);

Si nous changeons le nom de la fonction et le type comme ceci :

 extern double function(int num1, double num2);

vous n'auriez aucun problème à l'interpréter comme une fonction qui prend un int et un double comme arguments et renvoie un double (le feriez-vous ? Peut-être vaut-il mieux que vous ne l'avouiez pas si c'est un problème - mais peut-être devriez-vous faire attention à ne pas poser des questions aussi difficiles que celle-ci si c'est un problème).

Maintenant, au lieu d'être un double le signal() prend un SignalHandler comme second argument, et elle renvoie un comme résultat.

La mécanique par laquelle cela peut aussi être traité :

extern void (*signal(int signum, void(*handler)(int signum)))(int signum);

sont délicats à expliquer - je vais donc probablement me planter. Cette fois-ci, j'ai donné des noms aux paramètres - bien que ces noms ne soient pas essentiels.

En général, en C, le mécanisme de déclaration est tel que si vous écrivez :

type var;

alors quand vous écrivez var il représente une valeur de l'élément donné type . Par exemple :

int     i;            // i is an int
int    *ip;           // *ip is an int, so ip is a pointer to an integer
int     abs(int val); // abs(-1) is an int, so abs is a (pointer to a)
                      // function returning an int and taking an int argument

Dans la norme, typedef est traité comme une classe de stockage dans la grammaire, un peu à la manière de static y extern sont des classes de stockage.

typedef void (*SignalHandler)(int signum);

signifie que lorsque vous voyez une variable de type SignalHandler (disons alarm_handler) invoqué comme :

(*alarm_handler)(-1);

le résultat a type void - il n'y a pas de résultat. Et (*alarm_handler)(-1); est une invocation de alarm_handler() avec argument -1 .

Donc, si nous avons déclaré :

extern SignalHandler alt_signal(void);

cela signifie que :

(*alt_signal)();

représente une valeur nulle. Et donc :

extern void (*alt_signal(void))(int signum);

est équivalent. Maintenant, signal() est plus complexe car elle ne renvoie pas seulement un SignalHandler il accepte également un int et un SignalHandler comme arguments :

extern void (*signal(int signum, SignalHandler handler))(int signum);

extern void (*signal(int signum, void (*handler)(int signum)))(int signum);

Si cela vous déconcerte toujours, je ne sais pas comment vous aider - cela reste à certains niveaux mystérieux pour moi, mais je me suis habitué à son fonctionnement et je peux donc vous dire que si vous persévérez pendant encore 25 ans environ, cela deviendra une seconde nature pour vous (et peut-être même un peu plus rapidement si vous êtes intelligent).

3 votes

J'ai déjà vu cette explication auparavant. Et puis, comme c'est le cas maintenant, je pense que ce que je n'ai pas compris était le lien entre les deux déclarations : _extern void (signal(int, void()(int)))(int);/*et*/typedef void (*SignalHandler)(int signum); extern SignalHandler signal(int signum, SignalHandler handler); ou, ce que je veux demander, c'est quel est le concept sous-jacent que l'on peut utiliser pour arriver à la deuxième version que vous avez ? Quelle est la base qui connecte "SignalHandler" et le premier typedef ? Je pense que ce qui doit être expliqué ici est ce que fait réellement le typedef. Merci_

0 votes

@psychotik : L'article Wikipedia mentionne les typedefs et les pointeurs de fonctions, mais ne les illustre pas du tout. Peut-être que je devrais mettre une partie de cette réponse dans l'article Wikipedia :D

0 votes

Salut Jonathan, excellente réponse! J'utilise C et C++ pour implémenter des algorithmes mathématiques habituellement mais jamais pour programmer un OS ou d'autres tâches liées au système de bas niveau. Si cela était connu, cela aurait peut-être économisé un peu de frappe de votre part. Le cœur de votre réponse, pour moi, était lorsque vous avez commencé à clarifier le "define" en tant que classe de stockage dans la grammaire. Je ne suis familier qu'avec les typedefs habituels. Même si je suis familier avec les pointeurs sur les fonctions, mon problème résidait dans la syntaxe exacte des typedefs pour les pointeurs sur les fonctions. Vous avez bien résumé dans vos dernières phrases. La syntaxe nécessite un peu d'adaptation. Cordialement.

88voto

psychotik Points 11937

Un pointeur de fonction est comme tout autre pointeur, mais il pointe vers l'adresse d'une fonction au lieu de l'adresse de données (sur le tas ou la pile). Comme tout pointeur, il doit être correctement typé. Les fonctions sont définies par leur valeur de retour et les types de paramètres qu'elles acceptent. Ainsi, pour décrire pleinement une fonction, vous devez inclure sa valeur de retour et le type de chaque paramètre qu'elle accepte. Lorsque vous typedefinez une telle définition, vous lui donnez un 'nom convivial' qui rend plus facile la création et la référence de pointeurs utilisant cette définition.

Donc par exemple, supposons que vous avez une fonction :

float faireMultiplication (float num1, float num2 ) {
    return num1 * num2; }

alors le typedef suivant :

typedef float(*pt2Func)(float, float);

peut être utilisé pour pointer vers cette fonction faireMultiplication. Il définit simplement un pointeur vers une fonction qui renvoie un float et prend deux paramètres, chacun de type float. Cette définition a le nom convivial pt2Func. Notez que pt2Func peut pointer vers n'importe quelle fonction qui renvoie un float et prend 2 floats en entrée.

Ainsi, vous pouvez créer un pointeur qui pointe vers la fonction faireMultiplication comme suit :

pt2Func *monPointeurFn = &faireMultiplication;

et vous pouvez appeler la fonction en utilisant ce pointeur comme suit :

float resultat = (*monPointeurFn)(2.0, 5.1);

Cela se lit bien : http://www.newty.de/fpt/index.html

0 votes

Psychotik, merci! C'était utile. Le lien vers la page web des pointeurs de fonction est vraiment utile. En train de le lire maintenant.

0 votes

... Cependant, ce lien newty.de ne semble pas du tout parler des typedefs :( Donc même si ce lien est génial, les réponses dans ce fil de discussion sur les typedefs sont inestimables!

14 votes

Vous avez peut-être voulu faire pt2Func myFnPtr = &doMultiplication; au lieu de pt2Func *myFnPtr = &doMultiplication; car myFnPtr est déjà un pointeur.

37voto

Carl Norum Points 114072

cdecl est un excellent outil pour déchiffrer une syntaxe étrange comme les déclarations de pointeurs de fonction. Vous pouvez également l'utiliser pour les générer.

En ce qui concerne les astuces pour rendre les déclarations compliquées plus faciles à analyser pour une maintenance future (par vous-même ou par d'autres), je recommande de faire des typedef de petits morceaux et d'utiliser ces petits morceaux comme des blocs de construction pour des expressions plus grandes et plus compliquées. Par exemple :

typedef int (*FUNC_TYPE_1)(void);
typedef double (*FUNC_TYPE_2)(void);
typedef FUNC_TYPE_1 (*FUNC_TYPE_3)(FUNC_TYPE_2);

plutôt que :

typedef int (*(*FUNC_TYPE_3)(double (*)(void)))(void);

cdecl peut vous aider avec ce genre de choses :

cdecl> explain int (*FUNC_TYPE_1)(void)
declare FUNC_TYPE_1 as pointer to function (void) returning int
cdecl> explain double (*FUNC_TYPE_2)(void)
declare FUNC_TYPE_2 as pointer to function (void) returning double
cdecl> declare FUNC_TYPE_3 as pointer to function (pointer to function (void) returning double) returning pointer to function (void) returning int
int (*(*FUNC_TYPE_3)(double (*)(void )))(void )

Et c'est (en fait) exactement comment j'ai généré ce fouillis ci-dessus.

2 votes

Bonjour Carl, c'était un exemple et une explication très perspicaces. Merci également d'avoir montré l'utilisation de cdecl. Très apprécié.

0 votes

Y a-t-il un cdecl pour windows?

0 votes

@Jack, je suis sûr que tu peux le construire, oui.

36voto

user2786027 Points 61

Un moyen très simple de comprendre le typedef d'un pointeur de fonction:

int add(int a, int b)
{
    return (a+b);
}

typedef int (*add_integer)(int, int); //déclaration du pointeur de fonction

int main()
{
    add_integer addition = add; //typedef affecte une nouvelle variable c'est-à-dire "addition" à la fonction originale "add"
    int c = addition(11, 11);   //appel de la fonction via la nouvelle variable
    printf("%d",c);
    return 0;
}

13voto

int ajouter(int a, int b)
{
  return (a+b);
}
int soustraire(int a, int b)
{
  return (a-b);
}

typedef int (*fonction_math)(int, int); //declaration du pointeur de fonction

int main()
{
  fonction_math addition = ajouter;  //typedef assigne une nouvelle variable c'est-à-dire "addition" à la fonction originale "ajouter"
  fonction_math soustraction = soustraire; //typedef assigne une nouvelle variable c'est-à-dire "soustraction" à la fonction originale "soustraire"

  int c = addition(11, 11);   //appel de la fonction via la nouvelle variable
  printf("%d\n",c);
  c = soustraction(11, 5);   //appel de la fonction via la nouvelle variable
  printf("%d",c);
  return 0;
}

Résultat de ceci :

22

6

Remarquez que le même définisseur math_func a été utilisé pour déclarer les deux fonctions.

La même approche de typedef peut être utilisée pour externa struct. (utilisation de struct dans un autre fichier.)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X