web-dev-qa-db-fra.com

Comprendre les typedefs pour les pointeurs de fonction en C

J'ai toujours été un peu perplexe lorsque je lisais le code d'autres personnes, qui avait été typé pour les pointeurs vers des fonctions avec des arguments. Je me souviens que cela m'a pris un certain temps pour arriver à une telle définition tout en essayant de comprendre un algorithme numérique écrit en C il y a quelque temps. Alors, pourriez-vous partager vos conseils et vos réflexions sur la façon d’écrire de bons types de caractères pour les pointeurs vers des fonctions (à faire ou à ne pas faire), à ​​savoir pourquoi sont-ils utiles et comment comprendre le travail des autres? Merci!

219
user192712

Considérons la fonction signal() du standard C:

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

Parfaitement obscurément évident - c'est une fonction qui prend deux arguments, un entier et un pointeur vers une fonction qui prend un entier comme argument et qui ne renvoie rien, et celui-ci (signal()) renvoie un pointeur vers une fonction qui prend un entier comme argument et ne retourne rien.

Si vous écrivez:

typedef void (*SignalHandler)(int signum);

alors vous pouvez plutôt déclarer 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.

Cela prend 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 gestionnaire de signal à l'aide de SignalHandlertypedef dans la définition de la fonction.

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

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

La syntaxe moderne utilise seulement:

functionpointer(arg1, arg2, ...);

Je peux voir pourquoi cela fonctionne - je préfère simplement savoir que je dois rechercher où la variable est initialisée plutôt que pour une fonction appelée functionpointer.


Sam a commenté:

J'ai déjà vu cette explication. 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, quel est le concept sous-jacent que l’on peut utiliser pour proposer 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 typedef fait réellement ici.

Essayons encore. Le premier de ceux-ci provient directement de la norme C - je l'ai retapée et vérifié que les parenthèses étaient correctes (pas avant de l'avoir corrigé - c'est un cookie 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 sur une fonction qui prend un entier comme argument et qui ne renvoie rien.

La partie 'ne renvoie rien' est orthographiée void; l'argument qui est un entier est (je crois) explicite. La notation suivante est simplement (ou non) comment C épelle un pointeur vers une fonction en prenant les arguments tels que spécifiés et renvoyant le type donné:

type (*function)(argtypes);

Après avoir créé le type de gestionnaire de signal, 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);
}

Veuillez noter Comment éviter d'utiliser printf() dans un gestionnaire de signal?

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

Les deux premières fonctions sont des fonctions qui prennent un seul entier et ne renvoient rien. L’un d’eux ne revient pas du tout grâce à la exit(1);, mais l’autre revient après l’impression d’un message. Sachez que la norme C ne vous permet pas de faire beaucoup dans un gestionnaire de signal; POSIX est un peu plus généreux dans ce qui est autorisé, mais n'approuve pas officiellement l'appel à fprintf(). J'imprime également le numéro du signal reçu. Dans la fonction alarm_handler(), la valeur sera toujours SIGALRM car il s'agit du seul signal pour lequel il s'agit d'un gestionnaire, mais signal_handler() pourrait obtenir SIGINT ou SIGQUIT comme numéro de signal car la même fonction est utilisée tous 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 m'inquiéter de 3 signaux; Je me souciais souvent de SIGHUP, SIGPIPE et SIGTERM et de leur définition (#ifdef compilation conditionnelle), mais cela ne fait que compliquer les choses. J'utiliserais probablement aussi POSIX sigaction() au lieu de signal(), mais c'est un autre problème. restons avec ce que nous avons commencé avec.

La fonction main() effectue une itération sur la liste des gestionnaires à installer. Pour chaque gestionnaire, il appelle d'abord signal() pour savoir si le processus ignore actuellement le signal et, ce faisant, installe SIG_IGN en tant que gestionnaire, ce qui garantit que le signal reste ignoré. Si le signal n'était pas précédemment ignoré, il appelle à nouveau signal(), cette fois pour installer le gestionnaire de signal préféré. (L'autre valeur est probablementSIG_DFL, le gestionnaire de signal par défaut pour le signal.) En raison du premier appel à 'signal ()', définissez le gestionnaire sur SIG_IGN et signal() renvoie le gestionnaire d'erreur précédent. , la valeur de old après l'instruction if doit être SIG_IGN - d'où l'assertion. (Eh bien, cela pourrait être SIG_ERR si quelque chose se passait dramatiquement - mais j'apprendrais cela à la suite du tir d'affirmation.)

Le programme fait ensuite son travail et se termine normalement.

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

Donc, jusqu’à présent, j’ai montré qu’une variable SignalHandler est relativement simple à utiliser, tant que vous avez le type de valeur approprié à lui attribuer - c’est ce que fournissent les deux fonctions de gestionnaire de signal.

Revenons maintenant à la question: comment les deux déclarations de signal() sont-elles liées?.

Passons en revue la deuxième déclaration:

 extern SignalHandler signal(int signum, SignalHandler handler);

Si nous avons changé le nom de la fonction et le type comme ceci:

 extern double function(int num1, double num2);

vous n'auriez aucun problème à interpréter cela comme une fonction prenant comme argument int et double et renvoyant une valeur double (voudriez-vous peut-être mieux de ne pas laisser tomber si cela pose problème - mais peut-être devriez-vous être prudent en demandant des questions aussi difficiles que celle-ci s'il s'agit d'un problème).

Désormais, au lieu d'être double, la fonction signal() prend un SignalHandler comme second argument et elle en renvoie un comme résultat.

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

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

sont difficiles à expliquer - donc je vais probablement tout gâcher. Cette fois, j'ai donné les noms des paramètres - bien que les noms ne soient pas critiques.

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

type var;

alors, lorsque vous écrivez var, il représente une valeur du type donné. 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 le standard, typedef est traité comme une classe de stockage dans la grammaire, un peu comme static et extern sont des classes de stockage.

typedef void (*SignalHandler)(int signum);

signifie que lorsque vous voyez une variable de type SignalHandler (par exemple, alarm_handler) appelée 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 l'argument -1.

Donc, si nous déclarions:

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 il retourne non 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 confond encore, je ne sais pas comment vous aider - cela reste à certains niveaux mystérieux pour moi, mais je me suis habitué à la façon dont cela fonctionne et je peux donc vous dire que si vous vous en tenez à cela pendant encore 25 ans ou alors, cela deviendra une seconde nature pour vous (et peut-être même un peu plus vite si vous êtes malin).

278
Jonathan Leffler

Un pointeur de fonction est comme n'importe quel autre pointeur, mais il pointe sur l'adresse d'une fonction plutôt que sur l'adresse des données (tas ou pile). Comme tout pointeur, il doit être tapé correctement. Les fonctions sont définies par leur valeur de retour et les types de paramètres qu'elles acceptent. Donc, pour décrire complètement une fonction, vous devez inclure sa valeur de retour et le type de chaque paramètre est accepte. Lorsque vous entrez une telle définition, vous lui attribuez un "nom convivial" qui facilite la création et la référence de pointeurs utilisant cette définition.

Donc, par exemple, supposons que vous ayez une fonction:

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

puis le typedef suivant:

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

peut être utilisé pour pointer sur cette fonction doMulitplication. Il s'agit simplement de définir un pointeur sur une fonction qui renvoie un float et prend deux paramètres, chacun de type float. Cette définition porte le nom convivial pt2Func. Notez que pt2Func peut pointer sur N'IMPORTE quelle fonction qui retourne un float et prend 2 float.

Vous pouvez donc créer un pointeur qui pointe vers la fonction doMultiplication comme suit:

pt2Func *myFnPtr = &doMultiplication;

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

float result = (*myFnPtr)(2.0, 5.1);

Cela fait une bonne lecture: http://www.newty.de/fpt/index.html

73
psychotik

Un moyen très facile de comprendre le typedef du pointeur de fonction:

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

typedef int (*add_integer)(int, int); //declaration of function pointer

int main()
{
    add_integer addition = add; //typedef assigns a new variable i.e. "addition" to original function "add"
    int c = addition(11, 11);   //calling function via new variable
    printf("%d",c);
    return 0;
}
30
user2786027

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

En ce qui concerne les astuces pour faciliter l'analyse des déclarations compliquées pour une maintenance future (par vous-même ou par d'autres), je vous recommande de créer des typedefs de petits morceaux et d'utiliser ces petits morceaux comme blocs de construction pour des expressions plus volumineuses et plus complexes. 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 est (en fait) exactement comment j'ai généré ce foutu bordel ci-dessus.

27
Carl Norum
int add(int a, int b)
{
  return (a+b);
}
int minus(int a, int b)
{
  return (a-b);
}

typedef int (*math_func)(int, int); //declaration of function pointer

int main()
{
  math_func addition = add;  //typedef assigns a new variable i.e. "addition" to original function "add"
  math_func substract = minus; //typedef assigns a new variable i.e. "substract" to original function "minus"

  int c = addition(11, 11);   //calling function via new variable
  printf("%d\n",c);
  c = substract(11, 5);   //calling function via new variable
  printf("%d",c);
  return 0;
}

La sortie de ceci est:

22

6

Notez que la même définition de math_func a été utilisée pour la déclaration de la fonction.

La même approche de typedef peut être utilisée pour une structure externe (en utilisant sturuct dans un autre fichier).

12
Harshal Doshi Jain

C'est l'exemple le plus simple de pointeurs de fonction et de tableaux de pointeurs de fonction que j'ai écrit en tant qu'exercice.

    typedef double (*pf)(double x);  /*this defines a type pf */

    double f1(double x) { return(x+x);}
    double f2(double x) { return(x*x);}

    pf pa[] = {f1, f2};


    main()
    {
        pf p;

        p = pa[0];
        printf("%f\n", p(3.0));
        p = pa[1];
        printf("%f\n", p(3.0));
    }
4
Bing Bang

Utilisez typedefs pour définir des types plus complexes, par exemple des pointeurs de fonction

Je prendrai l'exemple de la définition d'une machine à états en C

    typedef  int (*action_handler_t)(void *ctx, void *data);

maintenant, nous avons défini un type appelé action_handler qui prend deux pointeurs et renvoie un int

définir votre machine d'état

    typedef struct
    {
      state_t curr_state;   /* Enum for the Current state */
      event_t event;  /* Enum for the event */
      state_t next_state;   /* Enum for the next state */
      action_handler_t event_handler; /* Function-pointer to the action */

     }state_element;

Le pointeur de fonction sur l'action ressemble à un type simple et typedef sert principalement cet objectif.

Tous mes gestionnaires d'événements doivent maintenant adhérer au type défini par action_handler

    int handle_event_a(void *fsm_ctx, void *in_msg );

    int handle_event_b(void *fsm_ctx, void *in_msg );

Références:

Programmation Expert C par Linden

3
vaaz