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 int
et a SignalHandler
et 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
typedef
dans 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 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 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 SIGALRM
car c'est le 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 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
et SIGTERM
aussi, de savoir si elles étaient définies ( #ifdef
compilation 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_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 probablement SIG_DFL
le gestionnaire de signal par défaut pour le signal.) Étant donné que le premier appel à 'signal ()' définit le gestionnaire SIG_IGN
et signal()
renvoie le gestionnaire d'erreur précédent, la valeur de old
après l' if
instruction 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_handler
c'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 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:
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 int
et un double
comme arguments et renvoie une double
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
, la signal()
fonction prend un SignalHandler
comme 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, var
il 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, typedef
est traité comme une classe de stockage dans la grammaire, plutôt 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 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 SignalHandler
les 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).