Розуміння typedefs для функціональних покажчиків на C


237

Я завжди був трохи спотиканий, коли читав код інших людей, який мав typedefs для покажчиків на функції з аргументами. Я пригадую, що мені знадобилося деякий час, щоб обійтись таким визначенням, намагаючись зрозуміти числовий алгоритм, написаний на С деякий час тому. Отже, ви могли б поділитися своїми порадами та думками щодо того, як написати хороші шрифти для покажчиків на функції (Do and Not’s), чому вони корисні та як зрозуміти роботу інших людей? Дякую!


1
Чи можете ви навести кілька прикладів?
Артелій

2
Ви не маєте на увазі typedefs для функціональних покажчиків, а не макроси для функціональних покажчиків? Я бачив перше, але не останнє.
dave4420

Відповіді:


297

Розглянемо 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);

у вас не виникне жодних проблем інтерпретувати це як функцію, яка бере аргументи inta і 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 років інакше це стане для вас другою природою (а може, навіть трохи швидше, якщо ви розумні).


3
Це пояснення я бачив і раніше. І тоді, як зараз, я думаю, що я не отримав - це зв'язок між двома твердженнями: extern void ( сигнал (int, void ( ) (int))) (int); / * та * / typedef void (* SignalHandler) (int signum); зовнішній сигнал SignalHandler (int signum, обробник SignalHandler); Або те, що я хочу запитати - це основна концепція, яку можна використати, щоб придумати другу версію, яку ви маєте? Що є основним, що з'єднує "SignalHandler" та перший typedef? Я думаю, що тут потрібно пояснити, що насправді робить typedef. Thx

6
Чудова відповідь, я радий, що повернувся до цієї теми. Я не думаю, що я все розумію, але одного разу зроблю. Ось чому мені подобається ТАК. Дякую.
Тото

2
Просто вибрати ніт: не безпечно дзвонити printf () та друзям всередині обробника сигналу; printf () не є повторним учасником (в основному тому, що він може викликати malloc (), що не є
ретентом

4
В extern void (*signal(int, void(*)(int)))(int);означає , що signal(int, void(*)(int))функція повертає покажчик на функцію до void f(int). Коли ви хочете вказати покажчик функції як повернене значення , синтаксис ускладнюється. Ви повинні розмістити тип повернутого значення ліворуч та список аргументів праворуч , тоді як це середина, яку ви визначаєте. І в цьому випадку сама signal()функція приймає вказівник функції як свій параметр, що ще більше ускладнює речі. Хороша новина - якщо ви можете прочитати цю, Сила вже з вами. :).
smwikipedia

1
Яка стара школа використовує &перед назвою функції? Це зовсім непотрібно; безглуздо, навіть. І точно не "стара школа". Стара школа використовує назву функції просто та просто.
Джонатан Леффлер

80

Покажчик функції, як і будь-який інший вказівник, але вказує на адресу функції замість адреси даних (на купі або стеку). Як і будь-який вказівник, його потрібно правильно ввести. Функції визначаються їх поверненим значенням та типом прийнятих параметрів. Отже, щоб повністю описати функцію, потрібно включити її повернене значення та тип кожного параметра прийнято. Коли ви вводите таке визначення, ви даєте йому "дружнє ім'я", що спрощує створення та посилання на покажчики, використовуючи це визначення.

Так, наприклад, припустимо, що у вас є функція:

float doMultiplication (float num1, float num2 ) {
    return num1 * num2; }

то наступний типdef:

typedef float(*pt2Func)(float, float);

можна вказувати на цю doMulitplicationфункцію. Це просто визначення вказівника на функцію, яка повертає поплавок і приймає два параметри, кожен з типів float. Це визначення має дружнє ім’я pt2Func. Зауважте, що pt2Funcможе вказувати на БУДЬ-яку функцію, яка повертає поплавок і приймає в два поплавці.

Таким чином, ви можете створити покажчик, який вказує на функцію doMultiplication наступним чином:

pt2Func *myFnPtr = &doMultiplication;

і ви можете викликати функцію за допомогою цього вказівника так:

float result = (*myFnPtr)(2.0, 5.1);

Це робить хорошим читання: http://www.newty.de/fpt/index.html


психотик, спасибі! Це було корисно. Посилання на веб-сторінку покажчиків функцій дуже корисно. Читаючи зараз.

... Однак ця посилання newty.de взагалі не говорить про typedefs :( Тож хоч це посилання чудово, але відгуки в цій темі про typedefs неоцінені!

11
Можливо, ви хочете зробити pt2Func myFnPtr = &doMultiplication;замість того, pt2Func *myFnPtr = &doMultiplication;як myFnPtrце вже вказівник.
Тамільсельван

1
оголошення pt2Func * myFnPtr = & doMultiplication; замість pt2Func myFnPtr = & doMultiplication; кидає попередження.
AlphaGoku

2
@Tamilselvan вірно. myFunPtrвже є функцією покажчика, тому використовуйтеpt2Func myFnPtr = &doMultiplication;
Дастін Бісер

35

Дуже простий спосіб зрозуміти typedef функції покажчика:

int add(int a, int b)
{
    return (a+b);
}

typedef int (*add_integer)(int, int); //declaration of function pointer

int main()
{
    add_integer addition = add; //typedef assigns a new variable i.e. "addition" to original function "add"
    int c = addition(11, 11);   //calling function via new variable
    printf("%d",c);
    return 0;
}

32

cdeclє чудовим інструментом для розшифровки дивних синтаксисів, таких як оголошення вказівників функції. Ви можете використовувати його і для їх генерування.

Що стосується порад щодо спрощення складних декларацій, щоб проаналізувати їх для подальшого обслуговування (самостійно або інших), я рекомендую робити typedefневеликі шматки та використовувати ці невеликі шматки як будівельні блоки для більших і складніших виразів. Наприклад:

typedef int (*FUNC_TYPE_1)(void);
typedef double (*FUNC_TYPE_2)(void);
typedef FUNC_TYPE_1 (*FUNC_TYPE_3)(FUNC_TYPE_2);

а не:

typedef int (*(*FUNC_TYPE_3)(double (*)(void)))(void);

cdecl може допомогти вам у цьому:

cdecl> explain int (*FUNC_TYPE_1)(void)
declare FUNC_TYPE_1 as pointer to function (void) returning int
cdecl> explain double (*FUNC_TYPE_2)(void)
declare FUNC_TYPE_2 as pointer to function (void) returning double
cdecl> declare FUNC_TYPE_3 as pointer to function (pointer to function (void) returning double) returning pointer to function (void) returning int
int (*(*FUNC_TYPE_3)(double (*)(void )))(void )

І чи є (насправді) саме те, як я породив той шалений безлад вище.


2
Привіт Карл, це був дуже проникливий приклад та пояснення. Також дякую за те, що показали використання cdecl. Цінується.

Чи є cdecl для windows?
Джек

@Jack, я впевнений, що ти можеш це створити, так.
Карл Норум

2
Існує також cdecl.org, який забезпечує такий самий потенціал, але в Інтернеті. Корисні для нас розробники Windows.
zaknotzach

12
int add(int a, int b)
{
  return (a+b);
}
int minus(int a, int b)
{
  return (a-b);
}

typedef int (*math_func)(int, int); //declaration of function pointer

int main()
{
  math_func addition = add;  //typedef assigns a new variable i.e. "addition" to original function "add"
  math_func substract = minus; //typedef assigns a new variable i.e. "substract" to original function "minus"

  int c = addition(11, 11);   //calling function via new variable
  printf("%d\n",c);
  c = substract(11, 5);   //calling function via new variable
  printf("%d",c);
  return 0;
}

Результатом цього є:

22

6

Зауважте, що для декларування обох функцій використовувався той самий математичний дефінітор.

Той самий підхід typedef може бути використаний для зовнішньої структури (використовуючи sturuct в іншому файлі.)


5

Використовуйте typedefs для визначення більш складних типів, тобто функціональних покажчиків

Я візьму приклад визначення стану машини в C

    typedef  int (*action_handler_t)(void *ctx, void *data);

тепер ми визначили тип під назвою action_handler, який бере два покажчики і повертає int

визначте свою державну машину

    typedef struct
    {
      state_t curr_state;   /* Enum for the Current state */
      event_t event;  /* Enum for the event */
      state_t next_state;   /* Enum for the next state */
      action_handler_t event_handler; /* Function-pointer to the action */

     }state_element;

Покажчик функції на дію виглядає як простий тип, а typedef в першу чергу служить цій цілі.

Усі мої обробники подій тепер повинні відповідати типу, визначеному action_handler

    int handle_event_a(void *fsm_ctx, void *in_msg );

    int handle_event_b(void *fsm_ctx, void *in_msg );

Список літератури:

Програмування Expert C від Linden


4

Це найпростіший приклад покажчиків функцій та масивів покажчиків функцій, які я написав як вправу.

    typedef double (*pf)(double x);  /*this defines a type pf */

    double f1(double x) { return(x+x);}
    double f2(double x) { return(x*x);}

    pf pa[] = {f1, f2};


    main()
    {
        pf p;

        p = pa[0];
        printf("%f\n", p(3.0));
        p = pa[1];
        printf("%f\n", p(3.0));
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.