当我阅读其他人的代码时,我总是有点难过,这些代码有 typedef 用于指向带参数的函数的指针。我记得在尝试理解前一段时间用 C 编写的数值算法时,我花了一段时间才找到这样的定义。那么,您能否分享您关于如何为函数指针(Do's 和 Do's)编写好的 typedef 的技巧和想法,以及它们为什么有用以及如何理解其他人的工作?谢谢!
7 回答
考虑signal()
C 标准中的函数:
extern void (*signal(int, void(*)(int)))(int);
非常明显 - 它是一个接受两个参数的函数,一个整数和一个指向一个函数的指针,该函数接受一个整数作为参数并且不返回任何内容,并且它 ( signal()
) 返回一个指向以整数作为参数并返回的函数的指针没有什么。
如果你写:
typedef void (*SignalHandler)(int signum);
那么你可以改为声明signal()
为:
extern SignalHandler signal(int signum, SignalHandler handler);
这意味着同样的事情,但通常被认为更容易阅读。更清楚的是,该函数接受 aint
和 aSignalHandler
并返回 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
,它的类型是:
一个指向函数的指针,该函数将整数作为参数并且不返回任何内容。
'return nothing' 部分是拼写的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);
}
那么,我们在这里做了什么——除了省略使代码编译干净所需的 4 个标准头之外?
前两个函数是接受单个整数并且不返回任何内容的函数。其中一个实际上根本没有返回,exit(1);
但另一个在打印消息后确实返回。请注意,C 标准不允许您在信号处理程序中做很多事情。POSIX在允许的方面更加慷慨,但官方并不认可 call fprintf()
。我还打印出收到的信号编号。在alarm_handler()
函数中,值将始终是SIGALRM
它作为处理程序的唯一信号,但signal_handler()
可能会获取SIGINT
或SIGQUIT
作为信号编号,因为两者都使用相同的函数。
然后我创建一个结构数组,其中每个元素标识一个信号编号和为该信号安装的处理程序。我选择担心 3 个信号;我经常担心它们是否被定义(条件编译) SIGHUP
,但这只会使事情复杂化。我也可能使用 POSIX而不是,但这是另一个问题;让我们坚持我们的开始。SIGPIPE
SIGTERM
#ifdef
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);
您可以毫无问题地将其解释为一个接受 anint
和 adouble
作为参数并返回一个double
值的函数(您会吗?如果这是有问题的,也许您最好不要“坦白” - 但也许您应该谨慎地提出问题如果这是一个问题,则作为这个)。
现在,double
该signal()
函数不再是 a ,而是将 aSignalHandler
作为其第二个参数,并返回一个作为结果。
也可以将其视为的机制:
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()
with argument的调用-1
。
所以,如果我们声明:
extern SignalHandler alt_signal(void);
代表着:
(*alt_signal)();
表示一个空值。因此:
extern void (*alt_signal(void))(int signum);
是等价的。现在,signal()
更复杂了,因为它不仅返回 a SignalHandler
,还接受 int 和 aSignalHandler
作为参数:
extern void (*signal(int signum, SignalHandler handler))(int signum);
extern void (*signal(int signum, void (*handler)(int signum)))(int signum);
如果这仍然让你感到困惑,我不知道如何提供帮助 - 它在某种程度上对我来说仍然很神秘,但我已经习惯了它的工作原理,因此可以告诉你,如果你再坚持 25 年左右,它会成为你的第二天性(如果你聪明的话,甚至可能会更快)。
函数指针与任何其他指针一样,但它指向函数的地址而不是数据的地址(在堆或堆栈上)。像任何指针一样,它需要正确输入。函数由它们的返回值和它们接受的参数类型定义。所以为了完整地描述一个函数,你必须包括它的返回值和每个参数的类型是接受。当您 typedef 这样的定义时,您给它一个“友好的名称”,这使得使用该定义创建和引用指针变得更加容易。
因此,例如假设您有一个功能:
float doMultiplication (float num1, float num2 ) {
return num1 * num2; }
然后是以下类型定义:
typedef float(*pt2Func)(float, float);
可以用来指向这个doMulitplication
函数。它只是定义一个指向函数的指针,该函数返回一个浮点数并接受两个参数,每个参数都是浮点数。此定义具有友好名称pt2Func
。请注意,pt2Func
可以指向任何返回浮点数并接受 2 个浮点数的函数。
因此,您可以创建一个指向 doMultiplication 函数的指针,如下所示:
pt2Func *myFnPtr = &doMultiplication;
您可以使用此指针调用该函数,如下所示:
float result = (*myFnPtr)(2.0, 5.1);
这很好读: http: //www.newty.de/fpt/index.html
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 )
并且(实际上)正是我如何在上面产生了那个疯狂的混乱。
理解函数指针的 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 的相同方法。(在其他文件中使用结构。)
使用typedef
's 定义更复杂的类型,即函数指针
我将以在 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 专家编程
This is the simplest example of function pointers and function pointer arrays that I wrote as an exercise.
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));
}