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).
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
Voir également Comment déclarer un pointeur de fonction __stdcall.