Розглянемо signal()
функцію зі стандарту С:
extern void (*signal(int, void(*)(int)))(int);
Ідеально незрозуміло очевидно - це функція, яка бере два аргументи, ціле число та покажчик на функцію, яка приймає ціле число як аргумент і нічого не повертає, і це (signal()
) повертає вказівник на функцію, яка приймає ціле число як аргумент і повертає нічого.
Якщо ви пишете:
typedef void (*SignalHandler)(int signum);
тоді ви можете замість цього оголосити signal()
як:
extern SignalHandler signal(int signum, SignalHandler handler);
Це означає те саме, але зазвичай його вважають дещо легшим для читання. Чіткіше, що функція приймає a int
і a SignalHandler
і повертає aSignalHandler
.
Хоча потрібно трохи звикнути. Єдине, що ви не можете зробити, хоча написати функцію обробника сигналу за допомогоюSignalHandler
typedef
визначення функції.
Я все ще з старої школи, яка вважає за краще викликати вказівник функції як:
(*functionpointer)(arg1, arg2, ...);
Сучасний синтаксис використовує просто:
functionpointer(arg1, arg2, ...);
Я бачу, чому це працює - я просто вважаю за краще знати, що мені потрібно шукати, де ініціалізована змінна, а не функція, що називається functionpointer
.
Сем прокоментував:
Це пояснення я бачив і раніше. І тоді, як зараз, я думаю, що я не отримав зв'язку між двома твердженнями:
extern void (*signal(int, void()(int)))(int); /*and*/
typedef void (*SignalHandler)(int signum);
extern SignalHandler signal(int signum, SignalHandler handler);
Або, що я хочу запитати - це основна концепція, яку можна використати, щоб придумати другу версію, яку ви маєте? Що є основним, що з'єднує "SignalHandler" та перший typedef? Я думаю, що тут потрібно пояснити, що насправді робить typedef.
Спробуємо ще раз. Перший з них знято прямо зі стандарту С - я його повторно ввів і перевірив, чи маю дужки правильно (не поки я не виправив - це важке печиво, щоб запам'ятати).
Перш за все, пам’ятайте, що typedef
вводиться псевдонім для типу. Отже, псевдонім є SignalHandler
, а його тип:
покажчик на функцію, яка приймає ціле число як аргумент і нічого не повертає.
Частина "нічого не повертає" написана void
; Аргумент, що є цілим числом, є (я довіряю), що пояснюється само собою. Наступне позначення - це просто (чи ні), як C пише вказівник на функціонування, беручи аргументи, як задано, і повертає заданий тип:
type (*function)(argtypes);
Після створення типу обробника сигналу я можу використовувати його для оголошення змінних тощо. Наприклад:
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);
}
Зверніть увагу, як уникнути використання printf()
в обробці сигналів?
Отже, що ми тут зробили - окрім опущення 4-х стандартних заголовків, які знадобляться для чистого збирання коду?
Перші дві функції - це функції, які беруть одне ціле число і нічого не повертають. Одна з них насправді взагалі не повертається завдяки exit(1);
другій, але інша повертається після друку повідомлення. Майте на увазі, що стандарт C не дозволяє вам робити дуже багато всередині обробника сигналів; POSIX трохи щедріший у тому, що дозволяється, але офіційно не санкціонує виклики fprintf()
. Я також роздруковую номер сигналу, який був отриманий. У alarm_handler()
функції значення завжди буде, SIGALRM
оскільки це єдиний сигнал, для якого це обробник, але signal_handler()
може отримати SIGINT
абоSIGQUIT
як номер сигналу, оскільки однакова функція використовується для обох.
Потім я створюю масив структур, де кожен елемент ідентифікує номер сигналу та обробник, який слід встановити для цього сигналу. Я вирішив потурбуватися про 3 сигнали; Я часто переживаю за те SIGHUP
, SIGPIPE
і SIGTERM
теж, і про те, чи визначені вони ( #ifdef
умовна компіляція), але це просто ускладнює речі. Я, мабуть, також використовував POSIX sigaction()
замість signal()
, але це інша проблема; давайте дотримаємось того, з чого ми почали.
У main()
функції перебирає список обробників , які будуть встановлені. Кожен обробник спочатку дзвонить, signal()
щоб з’ясувати, чи процес ігнорує сигнал в даний час, і при цьому він встановлює SIG_IGN
як обробник, що забезпечує, щоб сигнал залишався ігнорованим. Якщо сигнал раніше не ігнорувався, він signal()
повторно дзвонить , на цей раз встановити бажаний обробник сигналу. (Мабуть SIG_DFL
, інше значення - обробник сигналу за замовчуванням для сигналу.) Оскільки перший виклик "signal ()" встановлює обробник SIG_IGN
та signal()
повертає попередній обробник помилок, значення old
після того,if
заяви повинно бути SIG_IGN
- отже, і твердження. (Ну, могло бутиSIG_ERR
якщо щось різко пішло не так - але я б дізнався про це, коли стверджували, що стверджується.)
Потім програма виконує свої речі і нормально виходить.
Зауважте, що ім'я функції може розглядатися як вказівник на функцію відповідного типу. Якщо ви не застосуєте скобки функції-виклику - як, наприклад, в ініціалізаторах, - ім'я функції стає вказівником функції. Ось чому також доцільно викликати функції через pointertofunction(arg1, arg2)
позначення; коли ви бачите alarm_handler(1)
, ви можете вважати, що alarm_handler
це вказівник на функцію, а тому alarm_handler(1)
є викликом функції через функцію вказівника.
Таким чином, поки що я показав, що SignalHandler
змінна є відносно прямою вперед, якщо у вас є якесь правильне значення типу, яке потрібно присвоїти їй - саме це надає дві функції обробника сигналів.
Тепер ми повернемося до питання - як ці дві декларації signal()
стосуються один одного.
Розглянемо другу декларацію:
extern SignalHandler signal(int signum, SignalHandler handler);
Якщо ми змінили ім'я функції та тип типу:
extern double function(int num1, double num2);
у вас не виникне жодних проблем інтерпретувати це як функцію, яка бере аргументи int
a і a double
, і повертає double
значення (чи не так? Може, вам краще не зациклюватися, якщо це проблематично - але, можливо, вам слід бути обережними як запитання питань як цей, якщо це проблема).
Тепер, замість того, щоб бути a double
, signal()
функція приймає a SignalHandler
як свій другий аргумент, і повертає один як результат.
Механіка, за допомогою якої це також можна трактувати як:
extern void (*signal(int signum, void(*handler)(int signum)))(int signum);
важко пояснити - тож я, мабуть, це накручую. На цей раз я назвав імена параметрів - хоча імена не є критичними.
Загалом, у C механізм декларування такий, що якщо ви пишете:
type var;
тоді, коли ви пишете, var
це представляє значення заданого type
. Наприклад:
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
У стандарті typedef
трактується як клас зберігання в граматиці, швидше, як static
і extern
є класи зберігання.
typedef void (*SignalHandler)(int signum);
означає, що коли ви бачите змінну типу SignalHandler
(скажімо alarm_handler), яку викликають як:
(*alarm_handler)(-1);
результат має type void
- результату немає. І (*alarm_handler)(-1);
це виклик alarm_handler()
аргументу -1
.
Отже, якщо ми заявили:
extern SignalHandler alt_signal(void);
це означає, що:
(*alt_signal)();
являє собою порожнече значення. І таким чином:
extern void (*alt_signal(void))(int signum);
рівнозначно. Тепер, signal()
це складніше, тому що він не тільки повертає a SignalHandler
, він також приймає як int, так і SignalHandler
аргументи:
extern void (*signal(int signum, SignalHandler handler))(int signum);
extern void (*signal(int signum, void (*handler)(int signum)))(int signum);
Якщо це все ще бентежить вас, я не знаю, як допомогти - воно все ще на деяких рівнях загадкове для мене, але я звик до того, як це працює, і тому можу сказати вам, що якщо ви будете дотримуватися цього ще протягом 25 років інакше це стане для вас другою природою (а може, навіть трохи швидше, якщо ви розумні).