Comprendre les typedefs pour les pointeurs de fonction en C

237

J'ai toujours été un peu perplexe quand j'ai lu le code d'autres personnes qui avait des typedefs pour les pointeurs vers les fonctions avec des arguments. Je me souviens qu'il m'a fallu 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 caractères typographiques pour les pointeurs vers les fonctions (à faire et à ne pas faire), pourquoi ils sont utiles et comment comprendre le travail des autres? Merci!

nbro
la source
1
Pouvez-vous donner quelques exemples?
Artelius
2
Vous ne parlez pas de typedefs pour les pointeurs de fonction, au lieu de macros pour les pointeurs de fonction? J'ai vu le premier mais pas le dernier.
dave4420

Réponses:

297

Considérez la signal()fonction de la norme 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 ne renvoie rien, et elle ( signal()) renvoie un pointeur vers une fonction qui prend un entier comme argument et renvoie 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 an intet a SignalHandleret retourne a SignalHandler.

Il faut cependant s'y habituer un peu. La seule chose que vous ne pouvez pas faire, cependant, est d'écrire une fonction de gestionnaire de signal à l'aide de SignalHandler typedefdans la définition de 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 simplement:

functionpointer(arg1, arg2, ...);

Je peux voir pourquoi cela fonctionne - je préfère juste savoir que je dois chercher 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 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 typedef fait réellement ici.

Essayons encore. La première d'entre elles est directement extraite de la norme C - je l'ai retapée et vérifié que j'avais les bonnes parenthèses (pas jusqu'à ce que je les corrige - c'est un cookie difficile à retenir).

Tout d'abord, rappelez-vous que typedefintroduit 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 ne renvoie rien.

La partie «ne renvoie rien» est orthographiée void; l'argument qui est un entier est (je fais confiance) explicite. La notation suivante est simplement (ou non) comment le pointeur C orthographie la fonction en prenant les arguments comme spécifié et en retournant 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 et ainsi de suite. 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 signaux?

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'un d'eux ne revient pas du tout grâce à la 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 l'appel fprintf(). J'imprime également le numéro de signal reçu. Dans la alarm_handler()fonction, la valeur sera toujours SIGALRMcar c'est le seul signal pour lequel il s'agit d'un gestionnaire, mais signal_handler()pourrait obtenir SIGINTou SIGQUITcomme numéro de 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 soucier de 3 signaux; Je m'inquiétais souvent SIGHUP, SIGPIPEet SIGTERMaussi, de savoir si elles étaient définies ( #ifdefcompilation conditionnelle), mais cela complique les choses. J'utiliserais aussi probablement POSIX sigaction()au lieu de signal(), mais c'est un autre problème; restons avec ce que nous avons commencé.

La main()fonction parcourt 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 en SIG_IGNtant que gestionnaire, ce qui garantit que le signal reste ignoré. Si le signal n'était pas ignoré auparavant, il appelle à signal()nouveau, cette fois pour installer le gestionnaire de signal préféré. (L'autre valeur est probablement SIG_DFLle gestionnaire de signal par défaut pour le signal.) Étant donné que le premier appel à 'signal ()' définit le gestionnaire SIG_IGNet signal()renvoie le gestionnaire d'erreur précédent, la valeur de oldaprès l' ifinstruction doit être SIG_IGN- d'où l'assertion. (Eh bien, ça pourrait êtreSIG_ERR si quelque chose tournait mal - mais j'apprendrais cela du tir assert.)

Le programme fait ensuite son travail et se termine 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 initialiseurs, par exemple - le nom de la fonction devient un pointeur de fonction. C'est aussi pourquoi il est raisonnable d'invoquer des fonctions via la pointertofunction(arg1, arg2)notation; quand vous voyez alarm_handler(1), vous pouvez considérer que alarm_handlerc'est un pointeur vers la fonction et alarm_handler(1)est donc une invocation d'une fonction via un pointeur de fonction.

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

Maintenant, nous revenons à la question - comment les deux déclarations pour se signal()rapportent-elles l'une à l'autre?

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 à l'interpréter comme une fonction qui prend un intet un doublecomme arguments et renvoie une doublevaleur (le feriez-vous? peut-être que vous feriez mieux de ne pas vous tromper si cela est problématique - mais peut-être devriez-vous être prudent lorsque vous posez des questions aussi dures comme celui-ci si c'est un problème).

Maintenant, au lieu d'être a double, la signal()fonction prend un SignalHandlercomme deuxième argument et renvoie un comme résultat.

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

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

sont difficiles à expliquer - alors je vais probablement tout bousiller. 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;

puis lorsque vous écrivez, varil représente une valeur de la donnée 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, typedefest traité comme une classe de stockage dans la grammaire, plutôt comme staticet externsont 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 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 non seulement il retourne a SignalHandler, mais il accepte aussi SignalHandlerles arguments int et a as:

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

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

Si cela vous embrouille toujours, je ne sais pas comment aider - c'est encore à certains niveaux mystérieux pour moi, mais je me suis habitué à son fonctionnement et je peux donc vous dire que si vous vous en tenez à cela pendant 25 ans ou alors, cela deviendra une seconde nature pour vous (et peut-être même un peu plus vite si vous êtes intelligent).

Jonathan Leffler
la source
3
J'ai déjà vu cette explication. Et puis, comme c'est le cas maintenant, je pense que je n'ai pas obtenu la connexion entre les deux déclarations: extern void ( signal (int, void ( ) (int))) (int); / * et * / typedef void (* SignalHandler) (int signum); signal SignalHandler externe (int signum, gestionnaire SignalHandler); Ou, ce que je veux demander, 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 typedef fait réellement ici. Thx
6
Excellente réponse, je suis content d'être revenu sur ce fil. Je ne pense pas que je comprends tout, mais un jour je le ferai. C'est pourquoi j'aime SO. Je vous remercie.
toto
2
Juste pour choisir un nit: il n'est pas sûr d'appeler printf () et ses amis dans un gestionnaire de signaux; printf () n'est pas rentrant (essentiellement parce qu'il peut appeler malloc (), qui n'est pas rentrant)
wildplasser
4
Cela extern void (*signal(int, void(*)(int)))(int);signifie que la signal(int, void(*)(int))fonction renverra un pointeur de fonction vers void f(int). Lorsque vous souhaitez spécifier un pointeur de fonction comme valeur de retour , la syntaxe devient compliquée. Vous devez placer le type de valeur de retour à gauche et la liste d'arguments à droite , tandis que c'est le milieu que vous définissez. Et dans ce cas, la signal()fonction elle-même prend un pointeur de fonction comme paramètre, ce qui complique encore plus les choses. La bonne nouvelle est que si vous pouvez lire celui-ci, la Force est déjà avec vous. :).
smwikipedia
1
Quelle est la vieille école sur l'utilisation &devant un nom de fonction? C'est totalement inutile; inutile, même. Et certainement pas "old school". Old school utilise un nom de fonction clair et simple.
Jonathan Leffler
80

Un pointeur de fonction est comme tout autre pointeur, mais il pointe vers l'adresse d'une fonction au lieu de l'adresse des données (sur le tas ou la 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 accepté. Lorsque vous saisissez une telle définition, vous lui donnez un «nom convivial» qui facilite la création et la référence de pointeurs à l'aide de cette définition.

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 vers cette doMulitplicationfonction. Il s'agit simplement de définir un pointeur sur une fonction qui renvoie un flottant et prend deux paramètres, chacun de type flottant. Cette définition a le nom convivial pt2Func. Notez que pt2Funcpeut pointer vers N'IMPORTE QUELLE fonction qui renvoie un flottant et accepte 2 flotteurs.

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

pt2Func *myFnPtr = &doMultiplication;

et vous pouvez appeler la fonction à l'aide de ce pointeur comme suit:

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

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

psychotik
la source
psychotik, merci! C'était utile. Le lien vers la page Web des pointeurs de fonction est vraiment utile. Je le lis maintenant.
... Cependant, ce lien newty.de ne semble pas du tout parler des typedefs :( Donc, même si ce lien est génial, mais les réponses dans ce fil sur les typedefs sont inestimables!
11
Vous pourriez vouloir faire pt2Func myFnPtr = &doMultiplication;au lieu de pt2Func *myFnPtr = &doMultiplication;comme myFnPtrc'est déjà un pointeur.
Tamilselvan
1
déclarant pt2Func * myFnPtr = & doMultiplication; au lieu de pt2Func myFnPtr = & doMultiplication; lance un avertissement.
AlphaGoku
2
@Tamilselvan est correct. myFunPtrest déjà un pointeur de fonction, alors utilisezpt2Func myFnPtr = &doMultiplication;
Dustin Biser
35

Un moyen très simple 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;
}
user2786027
la source
32

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

En ce qui concerne les conseils 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 typedefs de petits morceaux et d'utiliser ces petits morceaux comme 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 désordre fou ci-dessus.

Carl Norum
la source
2
Salut Carl, c'était un exemple et une explication très perspicaces. Merci aussi d'avoir montré l'utilisation de cdecl. Très appréciée.
Existe-t-il cdecl pour Windows?
Jack
@Jack, je suis sûr que vous pouvez le construire, oui.
Carl Norum
2
Il existe également cdecl.org qui offre le même type de capacité mais en ligne. Utile pour nous, développeurs Windows.
zaknotzach
12
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 le même définisseur math_func a été utilisé pour déclarer à la fois la fonction.

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

Harshal Doshi Jain
la source
5

Utilisez des typedefs pour définir des types plus complexes, c'est-à-dire 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 retourne un int

définir votre machine à états

    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 effet.

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

vaaz
la source
4

Ceci est l'exemple le plus simple de pointeurs de fonction et de tableaux de pointeurs de fonction que j'ai écrit comme 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));
    }
Bing-bang
la source