最近新接触到的项目中非常频繁的使用了回调函数,查找了很多资料,在这里对我所理解的回调函数做一个总结。首先,对于回调函数提出几个问题:
什么是回调函数?
首先回调函数就是一个函数,它在形式上和其他函数没有区别,同样可以有参数和返回值。只不过这个所谓的回调函数的函数指针是将要被当做参数传递给另一个函数的,并被其调用。这时,我们就称这个被当做参数传递的函数为回调函数。
简单的说:"回调函数就是一个通过指针调用的函数,如果把函数指针作为参数传给另一个函数,当这个指针被用来调用所指向的函数时,我们就说这是回调函数。"
回调函数有什么作用?我们为什么要使用回调函数?
上面说到回调函数是把它的函数指针做为参数被调用,那么问题来了,为什么我们要把函数作为参数来调用呢,直接在函数体里面调用不行吗?也就是为什么我们要使用回调函数呢?其实“把函数做成参数”和“把变量做成参数”目的是一致的,就是以不变应万变。形参是不变的,而实参是变的。
在知乎上看到一个举例,非常形象的解释了回调函数:假设你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。
也就是说回调函数的作用就是,我们先在回调函数中定义当当一件我们不知道什么时候会发生的事件发生时需要干什么,当事件发生时,我们就可以调用回调函数处理这个事件。
什么时候使用回调函数?
一般在数据和网络通信中,当我们通信或数据传输时,由于我们不知道什么时候通信和数据传输会结束或者会中断,但是我们需要在结束或者中断时让程序得到相应的通知,并执行相应的动作。此时,就需有一个回调函数来进行回调,通知我们的程序事件已经发生,执行相应的动作。
另一种比较常用到的情况是,假设我们要编写一个库,它提供了某些排序算法的实现,如冒泡排序、快速排序、shell排序、选择排序等等,为了使库更加通用,可用于多种数据类型(int、float、string、char *),此时,该怎么办呢?可以使用函数指针,并进行回调。
怎么使用回调函数?
回调函数相当于一个中断处理函数,由系统或其他函数在符合我们设定的条件时自动调用。为此,我们需要做三件事:1,声明;2,定义;3,设置触发条件。就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用,具体的使用看下面。
函数指针
在理解“回调函数”之前,我们需要先讨论下函数指针的概念。
概念:函数和变量一样是存放在内存代码区域内的,它同样有地址,因此同样可以用指针来存取函数,把这种指向函数入口地址的指针称为函数指针。函数指针指向的是函数而非对象,和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定。例如:
// 定义一个比较两个string对象的长度的函数lengthCompare(),它的返回类型是bool
bool lengthCompare(const string&, const string&);
这个函数的类型是bool(const string&, const string&),那我们想要声明一个可以指向该函数的指针,只需要用指针替换函数名即可:
// pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
bool (*pf)(const string&, const string&);
// pf前面有个*,所以pf是一个指针,*pf两端的括号必不可少,如果不写,则pf是一个返回值为bool指针的函数
// 声明一个名为pf的函数,该函数返回bool*
bool *pf(const string&, const string&);
那么函数指针有什么用呢?我们在写一个函数时,虽然不能定义函数类型的形参,但是形参可以是函数类型的指针。
// 我们可以显示的将形参定义成指向函数的指针
void useBigger(const string &s1, const string &s2, bool (*pf)(const string&, const string&))
// 我们在调用这个函数时,可以直接把上面定义该类型的函数lengthCompare()的函数名作为实参使用,此时它会自动的转换成指针
useBigger(s1, s2, lengthCompare);
正如useBigger的声明语句所示,直接使用函数指针类型显得冗长而繁琐,如果我们要多次使用这个函数指针,每次都写这么长就会很麻烦,通常我们这样:
// 这样我们就可以用FuncP来表示一个参数是两个const string的引用,返回值是bool类型函数指针了
typedef bool (*FuncP)(const string&, const string&);
回调函数
1.不带参数的回调函数
#include <iostream>
typedef void (*CallBackFunc)();
void Func(CallBackFunc fPrintCB)
{
fPrintCB();
}
void CallBackprint()
{
std::cout << "CallBackprint" << std::endl;
}
int main(int argc,char* argv[])
{
Func(CallBackprint);
return 0;
}
2.带参数的回调函数
#include <iostream>
typedef void (*CallBackFunc)(int a);
void Func(CallBackFunc fPrintCB, int a)
{
fPrintCB(a);
}
void CallBackprint(int a)
{
std::cout << "CallBackprint : " << a << std::endl;
}
int main(int argc,char* argv[])
{
Func(CallBackprint, 123);
return 0;
}
和上面的函数指针里介绍的一样,有时候我们的回调函数有很多个参数,我们就可以使用typedef来简化
#include <iostream>
#include <string>
using std::string;
typedef void (*CallBackFunc)(int, string);
// 这里的函数参数只要写CallBackFunc就可以了,它代表了void (*CallBackprint)(int, string)
void Func(CallBackFunc fPrintCB, int a, string str)
{
fPrintCB(a, str);
}
void CallBackprint(int a, string str)
{
std::cout << "CallBackprint : " << a << str << std::endl;
}
int main(int argc,char* argv[])
{
Func(CallBackprint, 123 , "Hello");
return 0;
}
使用回调函数
回调函数最主要的用途就是当函数不处在同一个文件当中,比如动态链接库中,要调用其他程序中的函数就只有采用回调的形式。当一个应用程序运行起来时,一般情况下,应用程序会通过API直接调用动态链接库里写好的函数接口。但是有些库函数却要求应用程序先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数。
在上图中,我们可以看到,回调函数通常和应用程序处于同一抽象层(因为传入什么样的回调函数是在应用程序级别决定的)。而回调就成了一个高层调用底层,底层再回过头来调用高层的过程。