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!
237
Réponses:
Considérez la
signal()
fonction de la norme C: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:
alors vous pouvez plutôt déclarer
signal()
comme: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
int
et aSignalHandler
et retourne aSignalHandler
.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
typedef
dans la définition de fonction.Je suis toujours de la vieille école qui préfère invoquer un pointeur de fonction comme:
La syntaxe moderne utilise simplement:
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é:
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
typedef
introduit un alias pour un type. Ainsi, l'alias estSignalHandler
, et son type est: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é: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:
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'appelfprintf()
. J'imprime également le numéro de signal reçu. Dans laalarm_handler()
fonction, la valeur sera toujoursSIGALRM
car c'est le seul signal pour lequel il s'agit d'un gestionnaire, maissignal_handler()
pourrait obtenirSIGINT
ouSIGQUIT
comme 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
,SIGPIPE
etSIGTERM
aussi, de savoir si elles étaient définies (#ifdef
compilation conditionnelle), mais cela complique les choses. J'utiliserais aussi probablement POSIXsigaction()
au lieu designal()
, 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'abordsignal()
pour savoir si le processus ignore actuellement le signal et, ce faisant, installe enSIG_IGN
tant 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 probablementSIG_DFL
le gestionnaire de signal par défaut pour le signal.) Étant donné que le premier appel à 'signal ()' définit le gestionnaireSIG_IGN
etsignal()
renvoie le gestionnaire d'erreur précédent, la valeur deold
après l'if
instruction doit êtreSIG_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 voyezalarm_handler(1)
, vous pouvez considérer quealarm_handler
c'est un pointeur vers la fonction etalarm_handler(1)
est donc une invocation d'une fonction via un pointeur de fonction.Donc, jusqu'à présent, j'ai montré qu'une
SignalHandler
variable 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:
Si nous avons changé le nom de la fonction et le type comme ceci:
vous n'auriez aucun problème à l'interpréter comme une fonction qui prend un
int
et undouble
comme arguments et renvoie unedouble
valeur (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
, lasignal()
fonction prend unSignalHandler
comme deuxième argument et renvoie un comme résultat.La mécanique par laquelle cela peut également être traité comme:
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:
puis lorsque vous écrivez,
var
il représente une valeur de la donnéetype
. Par exemple:Dans la norme,
typedef
est traité comme une classe de stockage dans la grammaire, plutôt commestatic
etextern
sont des classes de stockage.signifie que lorsque vous voyez une variable de type
SignalHandler
(par exemple alarm_handler) appelée comme:le résultat a
type void
- il n'y a pas de résultat. Et(*alarm_handler)(-1);
est une invocation dealarm_handler()
avec argument-1
.Donc, si nous déclarions:
cela signifie que:
représente une valeur nulle. Et donc:
est équivalent. Maintenant,
signal()
est plus complexe car non seulement il retourne aSignalHandler
, mais il accepte aussiSignalHandler
les arguments int et a as: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).
la source
extern void (*signal(int, void(*)(int)))(int);
signifie que lasignal(int, void(*)(int))
fonction renverra un pointeur de fonction versvoid 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, lasignal()
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. :).&
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.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:
puis le typedef suivant:
peut être utilisé pour pointer vers cette
doMulitplication
fonction. 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 convivialpt2Func
. Notez quept2Func
peut 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:
et vous pouvez appeler la fonction à l'aide de ce pointeur comme suit:
Cela fait une bonne lecture: http://www.newty.de/fpt/index.html
la source
pt2Func myFnPtr = &doMultiplication;
au lieu dept2Func *myFnPtr = &doMultiplication;
commemyFnPtr
c'est déjà un pointeur.myFunPtr
est déjà un pointeur de fonction, alors utilisezpt2Func myFnPtr = &doMultiplication;
Un moyen très simple de comprendre le typedef du pointeur de fonction:
la source
cdecl
est 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
typedef
s de petits morceaux et d'utiliser ces petits morceaux comme blocs de construction pour des expressions plus grandes et plus compliquées. Par exemple:plutôt que:
cdecl
peut vous aider avec ce genre de choses:Et c'est (en fait) exactement comment j'ai généré ce désordre fou ci-dessus.
la source
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).
la source
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
maintenant, nous avons défini un type appelé action_handler qui prend deux pointeurs et retourne un int
définir votre machine à états
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
Références:
Programmation Expert C par Linden
la source
Ceci est l'exemple le plus simple de pointeurs de fonction et de tableaux de pointeurs de fonction que j'ai écrit comme exercice.
la source