Понимание typedefs для указателей на функции в C

Я всегда был немного озадачен, когда читал код других людей, который имел typedef для указателей на функции с аргументами. Я помню, что мне потребовалось некоторое время, чтобы обойти такое определение, пытаясь понять численный алгоритм, написанный на C некоторое время назад. Итак, не могли бы вы поделиться своими советами и мыслями о том, как написать хорошие определения типов для указателей на функции (что можно и нельзя), о том, почему они полезны и как понять работу других? Спасибо!

8 ответов

Рассмотрим signal() функция из стандарта C:

extern void (*signal(int, void(*)(int)))(int);

Совершенно непонятно очевидно - это функция, которая принимает два аргумента, целое число и указатель на функцию, которая принимает целое число в качестве аргумента и ничего не возвращает, и это (signal()) возвращает указатель на функцию, которая принимает целое число в качестве аргумента и ничего не возвращает.

Если вы напишите:

typedef void (*SignalHandler)(int signum);

тогда вы можете вместо этого объявить signal() как:

extern  SignalHandler signal(int signum, SignalHandler handler);

Это означает то же самое, но обычно считается более легким для чтения. Ясно, что функция принимает int и SignalHandler и возвращает SignalHandler,

Однако нужно немного привыкнуть. Единственное, что вы не можете сделать, это написать функцию обработчика сигнала, используя SignalHandlertypedef в определении функции.

Я все еще из старой школы, которая предпочитает вызывать указатель функции как:

(*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.

Давай еще раз попробуем. Первый из них взят прямо из стандарта C - я набрал его заново и проверил, что у меня были правильные скобки (пока я не исправил это - это сложный файл cookie, чтобы запомнить).

Прежде всего, помните, что 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 и double в качестве аргументов и возвращает double ценность (не так ли? возможно, вам лучше не признаваться, если это проблематично, но, возможно, вам следует быть осторожнее, задавая такие же сложные вопросы, как этот, если это проблема).

Теперь вместо того, чтобы быть double, signal() функция занимает 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() является более сложным, потому что он не только возвращает SignalHandler он также принимает как int, так и a SignalHandler в качестве аргументов:

extern void (*signal(int signum, SignalHandler handler))(int signum);

extern void (*signal(int signum, void (*handler)(int signum)))(int signum);

Если это все еще смущает вас, я не уверен, как помочь - это все еще на некоторых уровнях загадочно для меня, но я привык к тому, как это работает, и поэтому могу сказать вам, что если вы будете придерживаться этого еще 25 лет или так, это станет вашей второй натурой (и, может быть, даже немного быстрее, если вы умны).

Указатель на функцию похож на любой другой указатель, но он указывает на адрес функции, а не на адрес данных (в куче или стеке). Как и любой указатель, он должен быть набран правильно. Функции определяются их возвращаемым значением и типами параметров, которые они принимают. Таким образом, чтобы полностью описать функцию, вы должны включить ее возвращаемое значение и тип каждого параметра принимает. Когда вы вводите определение такого определения, вы присваиваете ему "понятное имя", которое облегчает создание и ссылку на указатели с использованием этого определения.

Например, предположим, у вас есть функция:

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

тогда следующий typedef:

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

может быть использован, чтобы указать на это doMulitplication функция. Это просто определение указателя на функцию, которая возвращает float и принимает два параметра, каждый из которых имеет тип float. Это определение имеет понятное название pt2Func, Обратите внимание, что pt2Func может указывать на ЛЮБУЮ функцию, которая возвращает число с плавающей точкой и принимает 2 числа с плавающей точкой.

Таким образом, вы можете создать указатель, который указывает на функцию doMultiplication следующим образом:

pt2Func *myFnPtr = &doMultiplication;

и вы можете вызвать функцию, используя этот указатель следующим образом:

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

Это делает хорошее чтение: http://www.newty.de/fpt/index.html

cdecl отличный инструмент для расшифровки странного синтаксиса, такого как объявления указателей на функции. Вы можете использовать его для их генерации.

Что касается советов по упрощению разбора сложных объявлений для последующего обслуживания (вами или другими), я рекомендую сделать typedef s маленьких кусков и использование этих маленьких кусочков в качестве строительных блоков для больших и более сложных выражений. Например:

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 )

И (фактически) именно так я и создал этот безумный беспорядок наверху.

Очень простой способ понять 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;
}
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

Обратите внимание, что один и тот же определитель math_func был использован для объявления обеих функций.

Тот же подход typedef может быть использован для extern struct.(Используя sturuct в другом файле.)

Используйте 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 );

Рекомендации:

Эксперт C программирование от Linden

Это простейший пример указателей функций и массивов указателей функций, которые я написал в качестве упражнения.

    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));
    }

А макрос? Я видел, что с typedef я не могу привести что-то к указателю на функцию. Поэтому я сделал для него этот небольшой макрос, который способен делать все, что мне нужно до сих пор (не знаю, на чем его нельзя использовать - я думаю, что это в основном typedef, но с необязательным параметром, чтобы сделать его пригодным для использования на слепках):

      #define funcptr_t(sym_name) void (*sym_name)(void)

// Declare a function pointer-returning function, and declare a function pointer variable.
funcptr_t (randomFunction(funcptr_t (func_ptr_variable)));
// Cast a variable to a function pointer
(funcptr_t()) some_variable;

PS: я сделал это для общего использования, не уверен, что его можно изменить для использования с конкретными функциями.

Другие вопросы по тегам