C语言陷阱与技巧25节,常说的“回调函数”是什么?为何要用它?

上一节主要讨论了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语言的灵魂一点也不夸张。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif