Rozważ signal()
funkcję ze standardu C:
extern void (*signal(int, void(*)(int)))(int);
Zupełnie niejasne oczywiste - jest to funkcja, która przyjmuje dwa argumenty, liczbę całkowitą i wskaźnik do funkcji, która przyjmuje liczbę całkowitą jako argument i nic nie zwraca, a ( signal()
) zwraca wskaźnik do funkcji, która przyjmuje liczbę całkowitą jako argument i zwraca nic.
Jeśli napiszesz:
typedef void (*SignalHandler)(int signum);
możesz zamiast tego zadeklarować signal()
jako:
extern SignalHandler signal(int signum, SignalHandler handler);
Oznacza to to samo, ale zwykle uważa się je za nieco łatwiejsze do odczytania. Jest bardziej zrozumiałe, że funkcja przyjmuje an int
i SignalHandler
a zwraca a SignalHandler
.
Trzeba jednak trochę przyzwyczaić się. Jednej rzeczy, której nie można zrobić, jest napisanie funkcji obsługi sygnału za pomocą SignalHandler
typedef
definicji funkcji.
Nadal jestem w starej szkole, która woli wywoływać wskaźnik funkcji, jak:
(*functionpointer)(arg1, arg2, ...);
Nowoczesna składnia używa tylko:
functionpointer(arg1, arg2, ...);
Rozumiem, dlaczego to działa - po prostu wolę wiedzieć, że muszę szukać miejsca, w którym inicjowana jest zmienna, niż wywoływać funkcję functionpointer
.
Sam skomentował:
Widziałem już to wyjaśnienie. A potem, tak jak teraz, myślę, że nie dostałem związku między tymi dwoma stwierdzeniami:
extern void (*signal(int, void()(int)))(int); /*and*/
typedef void (*SignalHandler)(int signum);
extern SignalHandler signal(int signum, SignalHandler handler);
Lub, chcę zapytać, jaka jest podstawowa koncepcja, której można użyć, aby wymyślić drugą wersję, którą masz? Jaka jest podstawa łącząca „SignalHandler” i pierwszy typedef? Myślę, że to, co należy wyjaśnić tutaj, to, co właściwie pisze tutaj typef.
Spróbujmy ponownie. Pierwszy z nich jest podnoszony prosto ze standardu C - przepisałem go i sprawdziłem, czy mam poprawne nawiasy (dopiero po poprawieniu - trudno zapamiętać ciasteczko).
Przede wszystkim pamiętaj, że typedef
wprowadza alias dla typu. Tak więc alias to SignalHandler
, a jego typ to:
wskaźnik do funkcji, która przyjmuje liczbę całkowitą jako argument i nic nie zwraca.
Część „nic nie zwraca” jest napisana void
; argument, który jest liczbą całkowitą, jest (ufam) oczywisty. Poniższa notacja jest po prostu (lub nie) sposobem, w jaki C literuje wskaźnik do funkcji, przyjmując argumenty zgodnie ze specyfikacją i zwracając dany typ:
type (*function)(argtypes);
Po utworzeniu typu modułu obsługi sygnału mogę go używać do deklarowania zmiennych i tak dalej. Na przykład:
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);
}
Uwaga Jak uniknąć używania printf()
w module obsługi sygnałów?
Co więc zrobiliśmy tutaj - pomijając 4 standardowe nagłówki, które byłyby potrzebne, aby kod był poprawnie kompilowany?
Pierwsze dwie funkcje to funkcje, które przyjmują jedną liczbę całkowitą i nic nie zwracają. Jeden z nich tak naprawdę wcale nie powraca, exit(1);
ale drugi wraca po wydrukowaniu wiadomości. Pamiętaj, że standard C nie pozwala ci robić wiele w procedurze obsługi sygnałów; POSIX jest nieco bardziej hojny w tym, co jest dozwolone, ale oficjalnie nie sankcjonuje połączeń fprintf()
. Wydrukowałem również otrzymany numer sygnału. W alarm_handler()
funkcji wartość będzie zawsze, SIGALRM
ponieważ jest to jedyny sygnał, dla którego jest to moduł obsługi, ale signal_handler()
może otrzymać SIGINT
lub SIGQUIT
jako numer sygnału, ponieważ ta sama funkcja jest używana w obu przypadkach.
Następnie tworzę tablicę struktur, w której każdy element identyfikuje numer sygnału i procedurę obsługi dla tego sygnału. Wybrałem martwić się o 3 sygnały; Ja często martwić SIGHUP
, SIGPIPE
a SIGTERM
także o tym, czy i one są zdefiniowane ( #ifdef
kompilacja warunkowa), ale to tylko komplikuje sprawę. Chciałbym również prawdopodobnie używać POSIX sigaction()
, zamiast signal()
, ale to już inna kwestia; trzymajmy się tego, od czego zaczęliśmy.
W main()
iteracje funkcyjne na listę ładowarki do zainstalowania. Dla każdego signal()
modułu obsługi najpierw wywołuje, aby dowiedzieć się, czy proces obecnie ignoruje sygnał, a jednocześnie robi to SIG_IGN
jako moduł obsługi, co zapewnia, że sygnał pozostaje ignorowany. Jeśli sygnał nie był wcześniej ignorowany, wówczas wywołuje signal()
ponownie, tym razem w celu zainstalowania preferowanej procedury obsługi sygnału. (Drugą wartością jest prawdopodobnie SIG_DFL
domyślna procedura obsługi sygnału). Ponieważ pierwsze wywołanie funkcji „signal ()” ustawia funkcję obsługi SIG_IGN
i signal()
zwraca poprzednią procedurę obsługi błędów, wartość old
po if
instrukcji musi być SIG_IGN
- stąd twierdzenie. (Cóż, może byćSIG_ERR
jeśli coś poszło dramatycznie nie tak - ale wtedy dowiedziałbym się o tym podczas strzelania z twierdzenia).
Następnie program wykonuje swoje czynności i kończy pracę normalnie.
Zauważ, że nazwę funkcji można traktować jako wskaźnik do funkcji odpowiedniego typu. Gdy nie zastosujesz nawiasów wywołania funkcji - jak na przykład w inicjalizatorach - nazwa funkcji staje się wskaźnikiem funkcji. Z tego powodu uzasadnione jest wywoływanie funkcji za pomocą pointertofunction(arg1, arg2)
notacji; kiedy widzisz alarm_handler(1)
, możesz uznać, że alarm_handler
jest to wskaźnik do funkcji, a zatem alarm_handler(1)
jest wywołaniem funkcji za pomocą wskaźnika funkcji.
Jak do tej pory pokazałem, że SignalHandler
zmienna jest stosunkowo prosta w użyciu, pod warunkiem, że masz do niej odpowiedni rodzaj wartości - właśnie to zapewniają dwie funkcje modułu obsługi sygnałów.
Teraz wracamy do pytania - jak te dwie deklaracje signal()
odnoszą się do siebie.
Przejrzyjmy drugą deklarację:
extern SignalHandler signal(int signum, SignalHandler handler);
Jeśli zmieniliśmy nazwę funkcji i typ w ten sposób:
extern double function(int num1, double num2);
to nie masz problemu z interpretacją tego jako funkcja, która trwa int
i double
jako argumenty i zwraca double
wartość (to być może, że nie jesteś lepszy „FESS się, czy to jest problematyczne - ale może należy być ostrożnym o zadawanie pytań, jak ciężko jak ten, jeśli jest to problem).
Teraz zamiast być a double
, signal()
funkcja przyjmuje SignalHandler
jako drugi argument i zwraca jeden jako wynik.
Mechanika, dzięki której można to również traktować jako:
extern void (*signal(int signum, void(*handler)(int signum)))(int signum);
są trudne do wyjaśnienia - więc prawdopodobnie to spieprzę. Tym razem podałem nazwy parametrów - choć nazwy nie są krytyczne.
Ogólnie rzecz biorąc, w C mechanizm deklaracji jest taki, że jeśli napiszesz:
type var;
wtedy, kiedy piszesz var
, reprezentuje wartość podaną type
. Na przykład:
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
Standardowo typedef
jest traktowane jako gramatyka jako klasa pamięci, a raczej jak static
i extern
są to klasy pamięci.
typedef void (*SignalHandler)(int signum);
oznacza, że gdy zobaczysz zmienną typu SignalHandler
(np. moduł obsługi alarmu) wywołaną jako:
(*alarm_handler)(-1);
wynik ma type void
- nie ma wyniku. I (*alarm_handler)(-1);
jest wywołanie alarm_handler()
z argumentem -1
.
Więc jeśli zadeklarujemy:
extern SignalHandler alt_signal(void);
to znaczy, że:
(*alt_signal)();
reprezentuje pustą wartość. I dlatego:
extern void (*alt_signal(void))(int signum);
jest równoważne. Teraz signal()
jest bardziej skomplikowany, ponieważ nie tylko zwraca a SignalHandler
, ale także przyjmuje zarówno SignalHandler
argumenty int, jak i jako:
extern void (*signal(int signum, SignalHandler handler))(int signum);
extern void (*signal(int signum, void (*handler)(int signum)))(int signum);
Jeśli nadal cię to dezorientuje, nie jestem pewien, jak pomóc - nadal jest dla mnie tajemnicze, ale przyzwyczaiłem się do tego, jak to działa i dlatego mogę powiedzieć, że jeśli będziesz go trzymać przez kolejne 25 lat a przynajmniej stanie się dla ciebie drugą naturą (a może nawet trochę szybciej, jeśli będziesz sprytny).