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

237

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

nbro
источник
1
Можете ли вы привести несколько примеров?
Артелиус
2
Разве вы не имеете в виду typedefs для указателей на функции вместо макросов для указателей на функции? Я видел первое, но не второе.
dave4420

Ответы:

297

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

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и возвращает a SignalHandler.

К этому нужно привыкнуть. Единственное, что вы не можете сделать, это написать функцию-обработчик сигнала, используя 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.

Давай еще раз попробуем. Первый из них взят прямо из стандарта 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);

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

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

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

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

Джонатан Леффлер
источник
3
Я видел это объяснение раньше. И затем, как и сейчас, я думаю, что я не получил связи между двумя операторами: extern void ( signal (int, void ( ) (int))) (int); / * и * / typedef void (* SignalHandler) (int signum); внешний сигнал SignalHandler (int signum, обработчик SignalHandler); Или, что я хочу спросить, какова основная концепция, которую можно использовать, чтобы придумать вторую версию, которая у вас есть? Что является фундаментальным, который связывает "SignalHandler" и первый typedef? Я думаю, что здесь нужно объяснить, что на самом деле здесь делает typedef. Thx
6
Отличный ответ, я рад, что вернулся к этой теме. Я не думаю, что я все понимаю, но однажды я пойму. Вот почему мне так нравится. Спасибо.
до
2
Просто чтобы прикинуть: небезопасно вызывать printf () и друзей внутри обработчика сигнала; printf () не является реентерабельным (в основном потому, что он может вызывать malloc (), который не реентерабелен)
wildplasser
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; }

тогда следующий 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

psychotik
источник
психотик, спасибо! Это было полезно. Ссылка на веб-страницу с указателями функций действительно полезна. Читаю это сейчас.
... Однако эта ссылка на 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;
}
user2786027
источник
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?
Джек,
@ Джек, я уверен, что ты можешь построить это, да.
Карл Норум
2
Существует также cdecl.org, который предоставляет такие же возможности, но онлайн. Полезно для нас разработчикам Windows.
закноцах
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

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

Тот же подход typedef может быть использован для extern struct. (Используя 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 );

Ссылки:

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

vaaz
источник
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));
    }
Бинг Банг
источник