上一节主要讨论了C语言中的函数指针在“运行时”代码选择中的应用,这其实是一个小技巧,仅需在需要切换代码的时候重新确定函数指针的指向,之后的代码就几乎不用动了。粗略来说,只需一次 if 判断,就可以将所有C语言代码涉及到的代码切换完成。这样的代码风格显然有利于程序员维护,也能提升C语言程序的运行效率。
事实上,C语言函数指针的用途远不止于此
在本专栏更早的章节中,我们曾讨论C语言函数的参数也可以是指针型的,“指针型”中的指针当然包括函数指针,也就是说,C语言函数的参数可以也是一个“函数”,只不过这个“函数”是通过函数指针传递的。请看下面这个例子:
#include <stdio.h>
void myprint()
{
printf("myprint\n");
}
void fun( void(*f)() )
{
f();
}
int main()
{
fun(&myprint);
return 0;
}
从上面这段C语言代码中,可以看出 fun() 函数接收一个参数,该参数是一个函数指针,指向返回值为空的函数。在 main() 中调用 fun() 时,将 myprint() 传递给它了。编译并执行这段C语言代码,得到如下输出:
# gcc t.c
# ./a.out
myprint
在 fun() 中调用的 myprint() 就是所谓的“回调函数”。显然,回调函数就是一个通过函数指针调用的函数,回调函数不是由实现方直接调用,而是通过函数指针,在特定条件发生时,由另外一方调用,用于对该条件响应。
上面的例子很简单,fun() 无条件调用 f 了,但是应明白,如果需要的话,程序员能够轻易为 f 的添加调用条件。
容易产生迷惑的点
在上述例子中,main() 函数中的 fun() 在接收函数指针时,fun(&myprint) 中的 & 符号可以不写。而且有些程序员在调用 f 时,为了显式的说明它是一个函数指针,常常写作:
(*f)();
但是也有程序员像本例一样,将函数指针当作普通函数使用:
f();
这似乎很不可思议,但是这些写法都可以正常工作,怎么回事呢?C语言的函数指针怎么会如此混乱不堪呢?
其实这主要是因为在C语言中,函数名,&函数名,以及 * 函数名在内存中的值是相等的,编写下面这样的C语言代码:
printf("%p, %p, %p\n", &myprint, myprint, *myprint);
编译并执行,得到如下输出:
0x40057d, 0x40057d, 0x40057d
显然,三者是相等的。所以究竟使用何种方式,主要取决于程序员自己的习惯了。
回调函数的意义
从上例可以看出,fun() 并不关心自己接收到的函数 f 以何种方式提供何种功能,这样一来,fun() 的一些功能就很灵活了。现在设想这种情况:
fun() 在处理数据时,需要用到排序算法,但是 fun() 的主要功能并不是排序,所以不打算在 fun() 中嵌入排序相关的C语言代码。
在这种情况下,回调函数就比较有用了,程序员可以在别处实现排序算法函数,再将该函数的地址以函数指针参数的形式传递给 fun() 就可以了。程序员甚至可以在别处实现若干个不同的排序算法函数(如冒泡排序、快速排序、shell排序、shake排序等等),根据实际情况,决定使用何种排序。
为什么不直接调用函数呢?感到迷惑的读者可以再看看上一节。
回调还可用于通知机制。例如,有时要在A程序中设置一个计时器,每到一定时间,A程序会得到相应的通知,但通知机制的实现者对A程序一无所知。那么,就需一个具有特定原型的函数指针进行回调,通知A程序事件已经发生。
回调函数的参数
上面的例子演示的 myprint() 没有参数,如果需要给回调函数传递参数,该怎么实现呢?请看下面的C语言代码:
void myprint(int a, double b)
{
printf("myprint recieve nums: %d, %0.2f\n", a, b);
}
void fun( void(*f)(), int a, double b )
{
f(a, b);
}
显然,可以在 fun() 中指定传递给 myprint() 的参数。如果需要传递给 myprint() 的参数比较多,则可以使用本专栏第21节提到的小技巧:借助指针和结构体:
struct param{
char a;
int b;
double c;
...
char str[128];
};
void myprint(void *data)
{
struct param *p = (struct param*)data;
printf("myprint recieve nums: %d, %0.2f...\n", p->a, p->b);
}
void fun( void(*f)(), void *data )
{
f(data);
}
显然,借助于C语言的指针和结构体语法,程序员可以仅使用一个参数,传递任意多的参数。事实上,一些比较成熟的库函数也是这么干的,例如:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
小结
本节主要讨论了C语言中的回调函数,应该能够发现,其实回调函数也是借助于C语言的指针语法实现的。另外,在文章最后还讨论了回调函数传递参数的方法,可以看出,借助指针和结构体语法,程序员能够轻易的传递任意多的复杂参数。归根结底,这些重要内容都离不开C语言中的指针,所以说指针是C语言的灵魂一点也不夸张。