一、调用和回调的定义
回调函数有点绕,可用简单的一个案例说明:
- A给B打电话,问:1+1等于几? B说,我也忘了,我得想一会,想好了再告诉你。这时比较好的方式是断开电话链接,等B想好了再告诉A。于是他们挂断了电话,A又去忙别的事情,B算好了答案,给A打电话,告诉A结果等于2。
- 上述,开始A给B打电话,后来,B想好了答案主动给A打电话的过程就是回调。
1.1 调用
- 同步调用, 是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;
- 异步调用, 是一种类似消息或事件的机制,不过它的调用方向刚好相反, 接口的服务在收到某种讯息或发生某种事件时,会主动通知客户端(即调用客户端的接口)。
1.2 回调
- 回调, 是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;
- 回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。
- 同步调用是三者当中最简单的,而回调又常常是异步调用的基础,
1.3 架构回调实现
下面我们集中比较具有代表性的语言(C)和架构(C++)来分析回调的实现方式、具体作用等。
1.3.1 函数指针
1.函数和函数指针:
//1111111111111111111
void Func(char *s);// 函数原型
void (*pFunc) (char *);//函数指针
//222222222222222222
int* func(int params, ...); //这就是指针函数
2.可以看出,函数的定义和函数指针的定义非常类似。 一般的化,为了简化函数指针类型的变量定义,提高程序的可读性,我们需要把函数指针类型自定义一下。
//1111111111111111111
typedef void(*pcb)(char *);
//222222222222222222
// 为回调函数命名,类型命名为 CallBackFun,参数为char *p 。
typedef int (*CallBackFun)(char *p);
3.回调函数可以象普通函数一样被程序调用,但是只有它被当作参数传递给被调函数时才能称作回调函数。
被回调函数的案例:
//11111111111111111
void GetCallBack(pcb callback)
{
/*do something*/
}
//2222222222222222
//下面的这些是调用者:有几种方式,看你喜欢哪种
int call(CallBackFun pCallBack, char *p)
{ // 执行回调函数,方式一:通过命名方式
printf("call 直接打印出字符%s!\n", p);
pCallBack(p);
return 0;
}
int call2(char *p, int (*ptr)())
{ // 执行回调函数,方式二:直接通过方法指针
printf("==============\n", p);
(*ptr)(p);
}
int call3(char *p, CallBackFun pCallBack)
{ // 执行回调函数,方式一:通过命名方式
printf("--------------\n", p);
pCallBack(p);
}
4.用户在调用上面的函数时,需要自己实现一个pcb类型的回调函数:
//111111111111111
void fCallback(char *s)
{
/* do something */
}
//222222222222222
int CallBackFun(char * p){
//TODO
}
5.然后,就可以直接把fCallback当作一个变量传递给GetCallBack,
//1111111111111
GetCallBack(fCallback);
//2222222222
//关联
6.如果赋了不同的值给该参数,那么调用者将调用不同地址的函数。
赋值可以发生在运行时,这样使你能实现动态绑定。
1.3.2 参数传递规则
- 到目前为止,我们只讨论了函数指针及回调而没有去注意ANSI C/C++的编译器规范。许多编译器有几种调用规范。
- 如在Visual C++中,可以在函数类型前加_cdecl,_stdcall或者_pascal来表示其调用规范(默认为_cdecl)。
- C++ Builder也支持_fastcall调用规范。
- 调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。
- 将调用规范看成是函数类型的一部分是很重要的;不能用不兼容的调用规范将地址赋值给函数指针。
例如:
// 被调用函数是以int为参数,以int为返回值
__stdcall int callee(int);
// 调用函数以函数指针为参数
void caller( __cdecl int(*ptr)(int));
// 在p中企图存储被调用函数地址的非法操作
__cdecl int(*p)(int) = callee; // 出错, 前者是__cdecl ,后者是__stdcall
指针p和callee()的类型不兼容,因为它们有不同的调用规范。
因此不能将被调用者的地址赋值给指针p,尽管两者有相同的返回值和参数列
1.4 回调函数的优缺点
1.4.1 优点
-
很多时候回调函数可以用来执行条件驱动的任务。即当该回调函数关心的那个条件被触发时,回调函数将被执行。条件触发可以是某一时间到了或者某一事件发生或者某一中断触发。
-
回调函数不是由该函数的实现方直接调用,而是在特定的条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
-
使程序设计更灵活。允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。而且这一设计允许了底层代码调用在高层定义的子程序。
-
回调函数最大的优势应用就是异步回调。
-
程序变成异步的了,也就是你不必再调用这个函数的时候一直等待这个时间的到达、事件的发生或中断的发生(万一一直不发生,你的程序会怎么样?)
-
在此期间你可以做别的事情,或者四处逛逛。当回调函数被执行时,你的程序重新得到执行的机会,此时你可以继续做必要的事情了。
1.4.2 缺点
- 学习成本会比普通函数高,需要有一定的抽象思维能力,需要对应用场景的理解。
- 回调函数很多情况下会附带有跨线程操作甚至于跨进程的操作,这些都是异步带来的成本。
2.案例
// 类A
class A{
public:
//A 的构造函数
A(){}
//函数指针类型自定义一下
typedef std::function<void(const std::shared_ptr<Submap2D>&)> UpdateFinishSubmapCallback;
//Test 函数,某种情况下会调用回调函数
void Test(){
std::shared_ptr<Submap2D> submaps;
if(submaps)
SetUpdateSubmapCallback(submaps);
}
//
void SetUpdateSubmapCallback(UpdateFinishSubmapCallback callback) {
updateFinishSubmapCallback_ = callback;
}
private:
//定义回调函数对象
UpdateFinishSubmapCallback updateFinishSubmapCallback_;
}
class B{
public:
A a_;
B(){
// B 构造时,将回调函数关联起来 UpdateFinishSubmapCallback std::placeholders::_1 指一个参数 ,多个参数 std::placeholders::_2...
a_.SetUpdateSubmapCallback(std::bind(&A::UpdateFinishSubmapCallback, this, std::placeholders::_1));
}
void spin(){
void (
// TODO
a_.Test();
}
private:
//声明和定义回调函数
bool UpdateFinishSubmapCallback(const std::shared_ptr<Submap2D>& submap){
//TODO
}
}
B b;
b.spin();
- 当B类调用 函数spin()时,会调用A中的 Test() 函数
- A中 Test() 一定条件会调用A的函数SetUpdateSubmapCallback()
- A的函数SetUpdateSubmapCallback() 调用回调函数 UpdateFinishSubmapCallback;
- A的UpdateFinishSubmapCallback 回调函数在B中调用,会调用B中bool
- UpdateFinishSubmapCallback(const std::shared_ptr& submap){} 函数