了解C语言中函数指针的typedef


237

当我阅读其他人的代码时,总是感到有些困惑,因为他们的代码带有用于带参数的函数的指针的typedef。我记得,花了一段时间才尝试理解这样的定义,同时又试图理解用C语言编写的数字算法。那么,您能否就如何为函数指针(“做”和“不做”)编写好的typedef共享您的提示和想法,以及它们为什么有用以及如何理解他人的工作?谢谢!


1
你能提供一些例子吗?
Artelius

2
您不是要对函数指针使用typedef,而是对函数指针使用宏吗?我见过前者,但没有见过。
dave4420,2009年

Answers:


297

考虑signal()C标准的功能:

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

显而易见,它是一个带有两个参数的函数,一个整数和一个指向以整数作为参数且不返回任何内容signal()的函数的指针,它()返回一个以整数作为参数并返回一个函数的指针没有。

如果您写:

typedef void (*SignalHandler)(int signum);

那么您可以改为声明signal()为:

extern  SignalHandler signal(int signum, SignalHandler handler);

这意味着同一件事,但通常被认为更容易阅读。更清楚的是,该函数采用intSignalHandler并返回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()可能会获得SIGINTSIGQUIT作为信号号。

然后,我创建一个结构数组,其中的每个元素都标识一个信号编号以及要为该信号安装的处理程序。我选择担心3个信号。我经常会担心它们SIGHUPSIGPIPE以及SIGTERM它们是否已定义(#ifdef条件编译),但这只会使事情变得复杂。我也可能会使用POSIX sigaction()代替signal(),但这是另一个问题。让我们坚持我们的开始。

main()函数遍历要安装的处理程序列表。对于每个处理程序,它首先调用signal()以查明进程当前是否正在忽略信号,并在此过程SIG_IGN中将其安装为处理程序,以确保信号保持忽略状态。如果该信号先前未被忽略,那么它将再次调用signal(),这一次是安装首选的信号处理程序。(另一个值可能是 SIG_DFL信号的默认信号处理程序。)由于第一次调用“ signal()” 会将处理程序设置为SIG_IGNsignal()返回前一个错误处理程序,因此oldafter 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);

您可以毫无疑问地将其解释为一个以an intdoubleas为参数并返回double值的函数(对吗?也许您最好不要烦恼这是否有问题-但也许您应该谨慎询问问题因为这是一个问题)。

现在,doublesignal()函数不再是a,而是将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在语法上被视为存储类,而不是staticextern是存储类。

typedef void (*SignalHandler)(int signum);

表示当您看到一个变量类型SignalHandler(例如alarm_handler)被调用为:

(*alarm_handler)(-1);

结果有type void-没有结果。并且(*alarm_handler)(-1);alarm_handler()with参数的调用-1

因此,如果我们声明:

extern SignalHandler alt_signal(void);

代表着:

(*alt_signal)();

代表无效值。因此:

extern void (*alt_signal(void))(int signum);

是等效的。现在,signal()它变得更加复杂,因为它不仅返回a 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年大约,这将成为您的第二天性(如果您很聪明,甚至可能会更快一点)。


3
我以前看过这种解释。然后,就像现在一样,我认为我没有得到的是两个语句之间的连接:extern void(signal(int,void()(int)))(int); / * and * / typedef void(* SignalHandler)(int信号); 外部SignalHandler signal(int signum,SignalHandler handler); 或者,我想问的是,一个人可以用来提出第二个版本的基本概念是什么?连接“ SignalHandler”和第一个typedef的基础是什么?我认为这里需要解释的是typedef在这里实际上在做什么。Thx

6
很好的答案,很高兴我回到了这个话题。我不了解所有事情,但是有一天我会。这就是为什么我喜欢SO。谢谢。
TOTO

2
只是选择一种选择:在信号处理程序内调用printf()和朋友并不安全;printf()不是可重入的(基本上是因为它可以调用malloc(),而后者不是可重入的)
wildplasser 2013年

4
extern void (*signal(int, void(*)(int)))(int);方法的signal(int, void(*)(int))函数会返回一个函数指针void f(int)。当您希望将函数指针指定为返回值时,语法会变得复杂。您必须将返回值类型放在左侧,将参数列表放在右侧,而这是您要定义的中间值。在这种情况下,signal()函数本身将函数指针作为其参数,这使事情更加复杂。好消息是,如果您能读懂这一点,那么部队已经在您身边了。:)。
smwikipedia 2014年

1
&在函数名称前使用的古老经验是什么?完全没有必要;毫无意义,甚至。绝对不是“老派”。老派使用简单而简单的函数名。
乔纳森·莱夫勒

80

函数指针与其他任何指针一样,但是它指向函数的地址而不是数据的地址(在堆或堆栈上)。像任何指针一样,它需要正确键入。函数由它们的返回值和它们接受的参数类型定义。因此,为了完整描述一个函数,您必须包括其返回值,并且每个参数的类型都可以接受。当键入defdef这样的定义时,给它一个“友好的名字”,这使使用该定义创建和引用指针变得更加容易。

例如,假设您具有一个功能:

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

然后是以下typedef:

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

可以用来指向此doMulitplication功能。它只是定义一个指向函数的指针,该函数返回一个float并接受两个参数,每个参数均为float类型。此定义具有友好的名称pt2Func。请注意,pt2Func可以指向ANY函数,该函数返回一个float并接受2个float。

因此,您可以创建一个指向doMultiplication函数的指针,如下所示:

pt2Func *myFnPtr = &doMultiplication;

您可以使用此指针如下调用函数:

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

这样可以很好地阅读:http : //www.newty.de/fpt/index.html


psychotik,谢谢!那很有帮助。指向功能指针网页的链接确实很有帮助。现在阅读。

...但是,该newty.de链接似乎根本没有谈论typedef :(因此,即使该链接很棒,但是此线程中有关typedef的响应还是非常宝贵的!

11
你可能会想要做pt2Func myFnPtr = &doMultiplication;的,而不是pt2Func *myFnPtr = &doMultiplication;因为myFnPtr已经是一个指针。
Tamilselvan 2015年

1
声明pt2Func * myFnPtr =&doMultiplication; 而不是pt2Func myFnPtr =&doMultiplication; 引发警告。
AlphaGoku

2
@Tamilselvan是正确的。 myFunPtr已经是一个函数指针,因此可以使用pt2Func myFnPtr = &doMultiplication;
Dustin Biser

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是解密奇怪的语法(如函数指针声明)的好工具。您也可以使用它来生成它们。

至于使复杂的声明易于解析以供将来维护(由您本人或其他人)的技巧,我建议将typedefs分成小块,并将这些小块用作较大和更复杂表达式的构建块。例如:

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的使用。非常感激。

Windows是否有cdecl?
2014年

@Jack,我确定您可以构建它,是的。
卡尔·诺鲁姆

2
还有cdecl.org,它提供相同的功能但在线。对我们Windows开发人员有用。
zaknotzach 2015年

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的相同方法用于外部结构(在其他文件中使用sturuct)。


5

使用typedef定义更复杂的类型,即函数指针

我将以在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 );

参考文献:

Linden的C专家编程


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));
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.