C语言里面最犀利的武器就是指针,其中一种比较特殊的指针叫做函数指针,关于函数指针有一种很广泛的应用叫做回调函数,这篇文章打算回答下面三个问题:
- 什么是回调函数?
- 回调函数怎么实现?
- 回调函数的应用场景?
1. 什么是回调函数?
Callback function technique is useful where you need to write a function that’s able to perform several different functions at a point or can perform a function specified only by the caller. This technique requires to pass a pointer-to-function to another routine, which calls back the user function to perform some task. Function calls back user’s function is called callback function.
用简洁的中文来表述回调函数就是,如果函数的参数中有函数指针,调用者可以传递一个函数的地址给实现者,让实现者去调用它,这称为回调函数。典型的例子就是stdlib.h
头文件中的qsort
:
void qsort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *));
2. 回调函数怎么实现?
回调函数的一个典型应用就是实现类似C++的泛型算法(Generics Algorithm)。下面实现的max
函数可以在任意一组对象中找出最大值,可以是一组int
、一组char
或者一组结构体,但是实现者并不知道怎样去比较两个对象的大小,调用者需要提供一个做比较操作的回调函数。max
函数之所以能对一组任意类型的对象进行操作,关键在于传给max
的是指向对象的指针所构成的数组,而不是对象本身所构成的数组,这样max
不必关心对象到底是什么类型,只需转给比较函数cmp
,然后根据比较结果做相应操作即可,cmp
是调用者提供的回调函数,调用者当然知道对象是什么类型以及如何比较。
/* generics.h */ #ifndef GENERICS_H #define GENERICS_H typedef int (*cmp_t)(void *, void *); extern void *max(void *data[], int num, cmp_t cmp); #endif /* generics.c */ #include "generics.h" void *max(void *data[], int num, cmp_t cmp) { int i; void *temp = data[0]; for(i=1; i<num; i++) { if(cmp(temp, data[i])<0) temp = data[i]; } return temp; } /* main.c */ #include <stdio.h> #include "generics.h" typedef struct { const char *name; int score; } student_t; int cmp_student(void *a, void *b) { if(((student_t *)a)->score > ((student_t *)b)->score) return 1; else if(((student_t *)a)->score == ((student_t *)b)->score) return 0; else return -1; } int main(void) { student_t list[4] = {{"Tom", 68}, {"Jerry", 72}, {"Moby", 60}, {"Kirby", 89}}; student_t *plist[4] = {&list[0], &list[1], &list[2], &list[3]}; student_t *pmax = max((void **)plist, 4, cmp_student); printf("%s gets the highest score %d\n", pmax->name, pmax->score); return 0; }
以上举例的回调函数是被同步调用的,调用者调用max函数,max函数则调用cmp函数,相当于调用者间接调了自己提供的回调函数。在实际系统中,异步调用也是回调函数的一种典型用法,调用者首先将回调函数传给实现者,实现者记住这个函数,这称为注册一个回调函数,然后当某个事件发生时实现者再调用先前注册的函数,比如sigaction(2)注册一个信号处理函数,当信号产生时由系统调用该函数进行处理,再比如pthread_create(3)注册一个线程函数,当发生调度时系统切换到新注册的线程函数中运行,在GUI编程中异步回调函数更是有普遍的应用,例如为某个按钮注册一个回调函数,当用户点击按钮时调用它。
3. 回调函数的应用场景?
软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。同步调用是三者当中最简单的,而回调又常常是异步调用的基础,因此,下面我们着重讨论回调机制在不同软件架构中的实现。
通常,当我们想通过一个统一接口实现不同内容的时候,用回调函数来实现就非常合适。任何时候,如果你所编写的函数必须能够在不同的时刻执行不同的类型的工作或者执行只能由函数调用者定义的工作,你都可以用回调函数来实现。许多窗口系统就是使用回调函数连接多个动作,如拖拽鼠标和点击按钮来指定调用用户程序中的某个特定函数。
回调函数可以理解为:上层对其下层的调用,回调函数的地址就是下层完成工作之后通知上层联络方式,回调函数的返回参数是下层完成工作之后返回上层的数据,回调函数的实现就是上层接到通知和数据之后自己随后好作的工作。
回调函数使用的场合:
1. 异步数据处理
2. 上下层之间的数据交换
从层次关系上看:是上层应用提供给下层应用的“接口”,由于是跨层、跨内存的,因此:回调函数只能以地址的形式传递,就像电话号码一样(管它是办公室电话、还是手机,反正打这个电话就能找到老板)。