Considera la signal()
funzione dello standard C:
extern void (*signal(int, void(*)(int)))(int);
Perfettamente oscuramente ovvio: è una funzione che accetta due argomenti, un numero intero e un puntatore a una funzione che accetta un numero intero come argomento e non restituisce nulla e signal()
restituisce un puntatore a una funzione che accetta un numero intero come argomento e restituisce Niente.
Se scrivi:
typedef void (*SignalHandler)(int signum);
quindi puoi invece dichiarare signal()
come:
extern SignalHandler signal(int signum, SignalHandler handler);
Ciò significa la stessa cosa, ma di solito è considerato un po 'più facile da leggere. È più chiaro che la funzione accetta an int
e a SignalHandler
e restituisce a SignalHandler
.
Ci vuole un po 'per abituarsi, però. L'unica cosa che non puoi fare, tuttavia, è scrivere una funzione del gestore del segnale usando SignalHandler
typedef
nella definizione della funzione.
Sono ancora della vecchia scuola che preferisce invocare un puntatore a funzione come:
(*functionpointer)(arg1, arg2, ...);
La sintassi moderna utilizza solo:
functionpointer(arg1, arg2, ...);
Posso capire perché funziona: preferisco solo sapere che devo cercare dove viene inizializzata la variabile piuttosto che per una funzione chiamata functionpointer
.
Sam ha commentato:
Ho visto questa spiegazione prima. E poi, come è il caso ora, penso che ciò che non ho ottenuto fosse la connessione tra le due affermazioni:
extern void (*signal(int, void()(int)))(int); /*and*/
typedef void (*SignalHandler)(int signum);
extern SignalHandler signal(int signum, SignalHandler handler);
Oppure, quello che voglio chiedere è, qual è il concetto di base che si può usare per inventare la seconda versione che hai? Qual è il fondamentale che collega "SignalHandler" e il primo typedef? Penso che ciò che deve essere spiegato qui sia ciò che sta facendo typedef qui.
Proviamo di nuovo. Il primo di questi viene sollevato direttamente dallo standard C - l'ho riscritto e verificato che le parentesi fossero corrette (non fino a quando non l'ho corretto - è un biscotto difficile da ricordare).
Prima di tutto, ricorda che typedef
introduce un alias per un tipo. Quindi, l'alias è SignalHandler
e il suo tipo è:
un puntatore a una funzione che accetta un numero intero come argomento e non restituisce nulla.
La parte "non restituisce nulla" è scritta void
; l'argomento che è un numero intero è (credo) autoesplicativo. La seguente notazione è semplicemente (o no) il modo in cui C fa il sillabatore puntando alla funzione prendendo argomenti come specificato e restituendo il tipo dato:
type (*function)(argtypes);
Dopo aver creato il tipo di gestore del segnale, posso usarlo per dichiarare le variabili e così via. Per esempio:
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);
}
Nota Come evitare l'uso printf()
in un gestore di segnale?
Quindi, cosa abbiamo fatto qui - a parte omettere 4 intestazioni standard che sarebbero necessarie per compilare il codice in modo pulito?
Le prime due funzioni sono funzioni che accettano un singolo numero intero e non restituiscono nulla. Uno di loro in realtà non ritorna affatto grazie al exit(1);
ma l'altro ritorna dopo aver stampato un messaggio. Essere consapevoli del fatto che lo standard C non consente di fare molto all'interno di un gestore di segnale; POSIX è un po 'più generoso in ciò che è permesso, ma ufficialmente non sanziona le chiamate fprintf()
. Stampo anche il numero del segnale ricevuto. Nella alarm_handler()
funzione, il valore sarà sempre SIGALRM
dato che è l'unico segnale per cui è un gestore, ma signal_handler()
potrebbe ottenere SIGINT
o SIGQUIT
come numero di segnale perché la stessa funzione viene utilizzata per entrambi.
Quindi creo una matrice di strutture, in cui ogni elemento identifica un numero di segnale e il gestore da installare per quel segnale. Ho scelto di preoccuparmi di 3 segnali; Mi preoccuperei spesso SIGHUP
, SIGPIPE
e SIGTERM
anche e se fossero definiti ( #ifdef
compilazione condizionale), ma ciò complica semplicemente le cose. Probabilmente userei POSIX sigaction()
invece di signal()
, ma questo è un altro problema; rimaniamo con ciò che abbiamo iniziato.
La main()
funzione scorre sull'elenco dei gestori da installare. Per ogni gestore, prima chiama signal()
per scoprire se il processo sta attualmente ignorando il segnale e, mentre lo fa, si installa SIG_IGN
come gestore, il che assicura che il segnale rimanga ignorato. Se in precedenza il segnale non veniva ignorato, viene richiamato signal()
, questa volta per installare il gestore di segnale preferito. (L'altro valore è presumibilmente SIG_DFL
, il gestore del segnale predefinito per il segnale.) Poiché la prima chiamata a "signal ()" imposta il gestore su SIG_IGN
e signal()
restituisce il precedente gestore degli errori, il valore di old
after if
dell'istruzione deve essere SIG_IGN
- da qui l'asserzione. (Beh, potrebbe essereSIG_ERR
se qualcosa fosse andato storto in modo drammatico, ma poi l'avrei appreso dall'asserzione.)
Il programma quindi fa le sue cose ed esce normalmente.
Si noti che il nome di una funzione può essere considerato come un puntatore a una funzione del tipo appropriato. Quando non si applicano le parentesi di chiamata di funzione, come ad esempio negli inizializzatori, il nome della funzione diventa un puntatore a funzione. Questo è anche il motivo per cui è ragionevole invocare funzioni tramite la pointertofunction(arg1, arg2)
notazione; quando vedi alarm_handler(1)
, puoi considerare che alarm_handler
è un puntatore alla funzione e quindi alarm_handler(1)
è un richiamo di una funzione tramite un puntatore a funzione.
Quindi, finora, ho dimostrato che una SignalHandler
variabile è relativamente semplice da usare, purché tu abbia del giusto tipo di valore da assegnare ad essa - che è ciò che forniscono le due funzioni del gestore del segnale.
Ora torniamo alla domanda: come si relazionano le due dichiarazioni signal()
.
Rivediamo la seconda dichiarazione:
extern SignalHandler signal(int signum, SignalHandler handler);
Se abbiamo cambiato il nome della funzione e il tipo in questo modo:
extern double function(int num1, double num2);
non avresti problemi a interpretare questo come una funzione che accetta un int
e un double
come argomenti e restituisce un double
valore (vuoi? forse faresti meglio a non preoccuparti se questo è problematico - ma forse dovresti essere cauto nel porre domande difficili come questo se è un problema).
Ora, invece di essere a double
, la signal()
funzione prende a SignalHandler
come secondo argomento e ne restituisce uno come risultato.
La meccanica con cui può anche essere trattata come:
extern void (*signal(int signum, void(*handler)(int signum)))(int signum);
sono difficili da spiegare - quindi probabilmente lo rovinerò. Questa volta ho dato i nomi dei parametri, sebbene i nomi non siano critici.
In generale, in C, il meccanismo di dichiarazione è tale che se si scrive:
type var;
quindi quando scrivi var
rappresenta un valore del dato type
. Per esempio:
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
Nello standard, typedef
viene considerato come una classe di memoria nella grammatica, piuttosto come static
e extern
sono classi di memoria.
typedef void (*SignalHandler)(int signum);
significa che quando vedi una variabile di tipo SignalHandler
(diciamo alarm_handler) invocata come:
(*alarm_handler)(-1);
il risultato ha type void
- non c'è risultato. Ed (*alarm_handler)(-1);
è un'invocazione di alarm_handler()
con argomento -1
.
Quindi, se dichiarassimo:
extern SignalHandler alt_signal(void);
significa che:
(*alt_signal)();
rappresenta un valore vuoto. E quindi:
extern void (*alt_signal(void))(int signum);
è equivalente. Ora signal()
è più complesso perché non solo restituisce a SignalHandler
, accetta anche SignalHandler
argomenti sia int che a as:
extern void (*signal(int signum, SignalHandler handler))(int signum);
extern void (*signal(int signum, void (*handler)(int signum)))(int signum);
Se questo ti confonde ancora, non sono sicuro di come aiutarti - è ancora ad alcuni livelli misterioso per me, ma mi sono abituato a come funziona e quindi posso dirti che se rimani con esso per altri 25 anni o giù di lì, diventerà una seconda natura per te (e forse anche un po 'più veloce se sei intelligente).