Betrachten Sie die signal()
Funktion aus dem C-Standard:
extern void (*signal(int, void(*)(int)))(int);
Völlig dunkel offensichtlich - es ist eine Funktion, die zwei Argumente akzeptiert, eine Ganzzahl und einen Zeiger auf eine Funktion, die eine Ganzzahl als Argument verwendet und nichts zurückgibt, und sie ( signal()
) gibt einen Zeiger auf eine Funktion zurück, die eine Ganzzahl als Argument verwendet und zurückgibt nichts.
Wenn Sie schreiben:
typedef void (*SignalHandler)(int signum);
dann können Sie stattdessen deklarieren signal()
als:
extern SignalHandler signal(int signum, SignalHandler handler);
Dies bedeutet dasselbe, wird jedoch normalerweise als etwas leichter lesbar angesehen. Es ist klarer, dass die Funktion a int
und a nimmt und a SignalHandler
zurückgibt SignalHandler
.
Es ist allerdings etwas gewöhnungsbedürftig. Das einzige, was Sie jedoch nicht tun können, ist das Schreiben einer Signalhandlerfunktion unter Verwendung der SignalHandler
typedef
in der Funktionsdefinition.
Ich gehöre immer noch zur alten Schule, die es vorzieht, einen Funktionszeiger wie folgt aufzurufen:
(*functionpointer)(arg1, arg2, ...);
Moderne Syntax verwendet nur:
functionpointer(arg1, arg2, ...);
Ich kann sehen, warum das funktioniert - ich möchte nur lieber wissen, dass ich suchen muss, wo die Variable initialisiert wird, anstatt nach einer aufgerufenen Funktion functionpointer
.
Sam kommentierte:
Ich habe diese Erklärung schon einmal gesehen. Und dann, wie jetzt, denke ich, was ich nicht verstanden habe, war die Verbindung zwischen den beiden Aussagen:
extern void (*signal(int, void()(int)))(int); /*and*/
typedef void (*SignalHandler)(int signum);
extern SignalHandler signal(int signum, SignalHandler handler);
Oder was ich fragen möchte, ist das zugrunde liegende Konzept, mit dem man die zweite Version entwickeln kann, die Sie haben? Was ist das Fundament, das "SignalHandler" und das erste typedef verbindet? Ich denke, was hier erklärt werden muss, ist, was typedef hier tatsächlich tut.
Lass es uns erneut versuchen. Die erste davon wird direkt aus dem C-Standard entfernt - ich habe sie erneut getippt und überprüft, ob die Klammern richtig waren (erst, nachdem ich sie korrigiert habe - es ist ein schwer zu merkender Cookie).
Denken Sie zunächst daran, dass typedef
ein Alias für einen Typ eingeführt wird. Der Alias ist also SignalHandler
und sein Typ ist:
Ein Zeiger auf eine Funktion, die eine Ganzzahl als Argument verwendet und nichts zurückgibt.
Der Teil "gibt nichts zurück" wird geschrieben void
. Das Argument, das eine ganze Zahl ist, ist (ich vertraue) selbsterklärend. Die folgende Notation ist einfach (oder nicht), wie C einen Zeiger auf eine Funktion buchstabiert, wobei die angegebenen Argumente verwendet und der angegebene Typ zurückgegeben werden:
type (*function)(argtypes);
Nachdem ich den Signalhandlertyp erstellt habe, kann ich damit Variablen deklarieren und so weiter. Beispielsweise:
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);
}
Bitte beachten Sie, wie Sie die Verwendung printf()
in einem Signalhandler vermeiden können .
Was haben wir hier also getan - abgesehen davon, dass 4 Standard-Header weggelassen wurden, die erforderlich wären, damit der Code sauber kompiliert werden kann?
Die ersten beiden Funktionen sind Funktionen, die eine einzelne Ganzzahl annehmen und nichts zurückgeben. Einer von ihnen kehrt dank dem überhaupt nicht zurück exit(1);
, der andere kehrt jedoch nach dem Drucken einer Nachricht zurück. Beachten Sie, dass der C-Standard es Ihnen nicht erlaubt, sehr viel in einem Signalhandler zu tun. POSIX ist etwas großzügiger in Bezug auf das, was erlaubt ist, sanktioniert jedoch offiziell keine Anrufe fprintf()
. Ich drucke auch die empfangene Signalnummer aus. In der alarm_handler()
Funktion ist der Wert immer SIGALRM
so, dass dies das einzige Signal ist, für das es sich um einen Handler handelt, das jedoch signal_handler()
möglicherweise SIGINT
oder erhältSIGQUIT
als Signalnummer, da für beide dieselbe Funktion verwendet wird.
Dann erstelle ich ein Array von Strukturen, in denen jedes Element eine Signalnummer und den für dieses Signal zu installierenden Handler identifiziert. Ich habe mich für 3 Signale entschieden. Ich würde oft Sorgen über SIGHUP
, SIGPIPE
und SIGTERM
auch , und darüber , ob sie (definiert sind #ifdef
bedingte Kompilierung), aber das erschwert nur die Dinge. Ich würde wahrscheinlich auch POSIX sigaction()
anstelle von verwenden signal()
, aber das ist ein anderes Problem. Bleiben wir bei dem, womit wir angefangen haben.
Die main()
Funktion durchläuft die Liste der zu installierenden Handler. Für jeden Handler wird zuerst aufgerufen signal()
, um herauszufinden, ob der Prozess das Signal derzeit ignoriert, und dabei SIG_IGN
als Handler installiert , wodurch sichergestellt wird, dass das Signal ignoriert bleibt. Wenn das Signal zuvor nicht ignoriert wurde, wird es signal()
erneut aufgerufen , diesmal um den bevorzugten Signalhandler zu installieren. (Der andere Wert ist vermutlich SIG_DFL
der Standard-Signalhandler für das Signal.) Da der erste Aufruf von 'signal ()' den Handler auf den vorherigen Fehlerhandler setzt SIG_IGN
und diesen signal()
zurückgibt, muss der Wert old
nach der if
Anweisung sein SIG_IGN
- daher die Behauptung. (Nun, es könnte seinSIG_ERR
wenn etwas dramatisch schief gelaufen ist - aber dann würde ich das aus dem Assert-Firing erfahren.)
Das Programm erledigt dann seine Aufgaben und wird normal beendet.
Beachten Sie, dass der Name einer Funktion als Zeiger auf eine Funktion des entsprechenden Typs angesehen werden kann. Wenn Sie die Klammern für Funktionsaufrufe nicht anwenden - wie zum Beispiel bei den Initialisierern - wird der Funktionsname zu einem Funktionszeiger. Aus diesem Grund ist es auch sinnvoll, Funktionen über die pointertofunction(arg1, arg2)
Notation aufzurufen . Wenn Sie sehen alarm_handler(1)
, können Sie davon ausgehen, dass dies alarm_handler
ein Zeiger auf die Funktion ist und daher alarm_handler(1)
ein Aufruf einer Funktion über einen Funktionszeiger.
Bisher habe ich gezeigt, dass die Verwendung einer SignalHandler
Variablen relativ einfach ist, solange Sie den richtigen Werttyp zuweisen können - genau das bieten die beiden Signalhandlerfunktionen.
Nun kommen wir zurück zu der Frage, in welcher signal()
Beziehung die beiden Erklärungen zueinander stehen.
Sehen wir uns die zweite Erklärung an:
extern SignalHandler signal(int signum, SignalHandler handler);
Wenn wir den Funktionsnamen und den Typ wie folgt geändert haben:
extern double function(int num1, double num2);
Sie hätten kein Problem damit, dies als eine Funktion zu interpretieren, die ein int
und ein double
als Argumente double
annimmt und einen Wert zurückgibt (oder? Vielleicht sollten Sie sich nicht ein Bild machen, wenn dies problematisch ist - aber vielleicht sollten Sie vorsichtig sein, wenn Sie Fragen so hart stellen wie dieser, wenn es ein Problem ist).
Anstatt a zu sein double
, nimmt die signal()
Funktion a SignalHandler
als zweites Argument und gibt eins als Ergebnis zurück.
Die Mechanik, mit der das auch behandelt werden kann als:
extern void (*signal(int signum, void(*handler)(int signum)))(int signum);
sind schwierig zu erklären - also werde ich es wahrscheinlich vermasseln. Diesmal habe ich die Parameternamen angegeben - obwohl die Namen nicht kritisch sind.
Im Allgemeinen ist der Deklarationsmechanismus in C so, dass, wenn Sie schreiben:
type var;
dann, wenn Sie schreiben var
, repräsentiert es einen Wert des Gegebenen type
. Beispielsweise:
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
Im Standard typedef
wird als Speicherklasse in der Grammatik behandelt, eher wie static
und extern
sind Speicherklassen.
typedef void (*SignalHandler)(int signum);
bedeutet, dass, wenn Sie eine Variable vom Typ SignalHandler
(z. B. alarm_handler) sehen, die wie folgt aufgerufen wird:
(*alarm_handler)(-1);
Das Ergebnis hat type void
- es gibt kein Ergebnis. Und (*alarm_handler)(-1);
ist eine Anrufung alarm_handler()
mit Argument -1
.
Also, wenn wir erklärt haben:
extern SignalHandler alt_signal(void);
es bedeutet, dass:
(*alt_signal)();
stellt einen leeren Wert dar. Und deshalb:
extern void (*alt_signal(void))(int signum);
ist gleichwertig. Jetzt signal()
ist es komplexer, weil es nicht nur a zurückgibt SignalHandler
, sondern auch sowohl ein int als auch ein a SignalHandler
als Argumente akzeptiert :
extern void (*signal(int signum, SignalHandler handler))(int signum);
extern void (*signal(int signum, void (*handler)(int signum)))(int signum);
Wenn Sie das immer noch verwirrt, bin ich mir nicht sicher, wie ich Ihnen helfen soll - es ist mir immer noch auf einigen Ebenen rätselhaft, aber ich habe mich daran gewöhnt, wie es funktioniert, und kann Ihnen daher sagen, wenn Sie noch 25 Jahre dabei bleiben oder so, es wird für Sie zur zweiten Natur (und vielleicht sogar ein bisschen schneller, wenn Sie klug sind).