回调函数的概念、意义和应用场景
概念
回调函数,就是使用者自己定义一个函数,并实现函数的内容,然后把这个函数作为参数传入其它函数中,由其它函数在运行时来调用。
换句话说,函数是你实现的,但由别人的函数在运行时通过参数传递的方式调用,这就是所谓的回调函数。
为什么要用回调函数?
这是一种设计策略。我们想象一种系统实现:
在一个下载系统中,有一个文件下载模块、一个下载文件当前进度显示模块,系统要求实时显示文件的下载进度,想想很简单,在面向对象的世界里,无非是实现两个类而已。
但是问题恰恰出在这里。显示模块如何驱动下载进度条?
文件下载进度只有下载模块才知道。显示模块不知道,也不应该知道下载模块的进度,这是面向对象“高内聚、低耦合”的设计特性决定的。
解决方案很简单:
给下载模块传递一个函数指针,作为回调函数,驱动显示模块的显示进度。
在面向对象的世界中这样的例子还真不少。
造成这样问题的根源,就是面向对象的程序设计思想,要求模块独立性、高内聚低耦合等特性。
回调函数机制
- 定义一个函数(普通函数即可);
- 将此函数的地址注册给调用者;
- 特定的事件或条件发生时,调用者使用函数指针调用回调函数。
不带参数的回调函数
一个标准Hello World程序如下:
int main(int argc,char* argv[]){
printf("Hello World!\n");
return 0;
}
将它修改成函数回调样式:
//定义回调函数
void PrintfText() {
printf("Hello World!\n");
}
//定义实现回调函数的"调用函数"
void CallPrintfText(void (*callfuct)()){
callfuct();
}
//在main函数中实现函数回调
int main(int argc,char* argv[]){
CallPrintfText(PrintfText);
return 0;
}
带参数的回调函数
//定义带参回调函数
void PrintfText(char* s) {
printf(s);
}
//定义实现带参回调函数的"调用函数"
void CallPrintfText(void (*callfuct)(char*), char* s){
callfuct(s);
}
//在main函数中实现带参的函数回调
int main(int argc,char* argv[]){
CallPrintfText(PrintfText,"Hello World!\n");
return 0;
}
应用场景
扩展库
假设有这样一种情况:
我们要编写一个库,它需要排序算法(如冒泡排序、快速排序、shell排序、shake排序等等),为了能让库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑。
此时,就可以把排序算法定义成回调函数,由库来调用。
定时通知机制
例如,在A模块中设置一个计时器,定期调用B模块提供的回调函数,这就是一种通知机制。
- 编写好一个定时器模块,到了时间就触发一次调用。
- 同样地,提供1)回调函数定义;2)注册回调函数接口。
- 应用者把自己需要定时处理的过程编写成一个实际函数,需要满足回调函数的定义,注册到定时器。
底层模块调用高层模块,如驱动
底层模块调用高层模块,但高层模块还没写好,或者说,底层模块根本不需要知道高层模块是怎么编写的,这个时候用回调函数是最好的。
如,驱动收到数据后,调用高层应用处理,就属于这种场景。
- 驱动层提供1)回调函数定义,2)注册回调函数的接口。
- 应用层1)编写回调函数的实现;2)调用驱动层的注册接口将实现的回调函数指针注册给驱动层。
- 当驱动层收到数据后,调用所注册的回调函数,处理数据。
总结
如果一个对象关心另一个对象的状态变化,那么,给状态的变化注册回调函数,让它通知你这类状态的改变,这样,在封装了模块变化的同时,实现了模块间的协作关系,另辟独径地给对象之间进行了解耦。