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

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

Ответ 1

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

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

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

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

typedef void (*SignalHandler)(int signum);

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

extern  SignalHandler signal(int signum, SignalHandler handler);

Это означает то же самое, но обычно считается несколько более легким для чтения. Понятно, что функция принимает 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);

у вас не возникло бы проблемы с интерпретацией этого как функции, которая принимает аргументы 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)();

представляет значение void. И поэтому:

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 лет или около того, это станет для вас второй натурой (а может быть, даже немного быстрее, если вы умны).

Ответ 2

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

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

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

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

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

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

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

pt2Func *myFnPtr = &doMultiplication;

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

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

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

Ответ 3

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 )

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

Ответ 4

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

Ответ 5

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 в другом файле.)

Ответ 6

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

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