出处1:https://blog.youkuaiyun.com/u014078216/article/details/49717405
在C/C++里面,函数的概念很好理解,就是把某个任务独立出来,封装在一起,然后给它取个名字,它可以有参数和返回值。那么,回调函数是个什么鬼呢?它和函数到底有何异同?既然已经有了函数,为啥还非要生出个回调函数来?
首先,回调函数也是函数,就像白马也是马一样。它具有函数的所有特征,它可以有参数和返回值。其实,单独给出一个函数是看不出来它是不是回调函数的。回调函数区别于普通函数在于它的调用方式。只有当某个函数(更确切的说是函数的指针)被作为参数,被另一个函数调用时,它才是回调函数。就像给你一碗饭,你并不能说它是中饭还是晚饭一样,只有当你在某个时候把它吃掉了你才明确它是中饭还是晚饭(这个比喻貌似有点挫。领会精神就好,哈哈)。
【注释】:函数指针是一个指向函数的指针变量,它是专门来存放函数入口地址的,在程序中给它赋予哪个函数的入口地址,它就指向哪个函数,因此在一个程序中,一个函数的指针可被多次赋值,指向不同的函数。
那么问题来了,为什么我们要把函数作为参数来调用呢,直接在函数体里面调用不好吗?这个问题问的好。在这个意义上,“把函数做成参数”和“把变量做成参数”目的是一致的,就是以不变应万变。形参是不变的,而实参是变的。唯一不同的是,普通的实参可以由计算机程序自动产生,而函数这种参数计算机程序是无法自己写出来的,因为函数本身就是程序(要是程序可以写程序的话那就是超级人工智能了),它必须由人来写。所以对于回调函数这种参数而言,它的“变”在于人有变或者人的需求有变。
C++ Primer里面举了个例子就是排序算法。为了使排序算法适应不同类型的数据,并且能够按各种要求进行排序,机智的人类把排序算法做成了一个模版(在标准模版库STL里),并且把判断两个数据之间的“大小”(也可以是“字节数”,或者其他某种可以比较的属性)这个任务(即函数)当成一个参数放在排序算法这个函数的参数列表里,而把它的具体实现就交给了使用排序算法的人。这个判断大小的函数就是一个回调函数。比如我们要给某个vector容器里面的单词进行排序,我们就可以声明一个排序算法:
void stable_sort(vector<string>::iterator iterBegin,
vector<string>::iterator iterEnd,
bool (*isShorter)(constr string &,const string &));
其中前面两个是普通参数,即迭代器(用于标记vector容器里面元素的位置),而第三个参数isShorter就是回调函数。根据不同需求isShorter可以有不同的实现,包括函数名。比如:
bool myIsShorter(const string &s1, const string &s2)
{
return s1.size()<s2.size();
}
stable_sort(words.begin(),words.end(),myIsShorter);
根据需求你也可以换一种方式来实现。
注意,在传递myIsShorter这个参数时,只需写函数名,它代表函数指针。
后面绝对不能加()和参数,绝对不能加()和参数,绝对不能加()和参数!因为那样是调用函数的返回值!两者天壤之别!
在stable_sort运行时,当遇到需要比较两个单词的长短时,就会对myIsShorter进行调用,得到一个判断。
在调用时,还必须把两个参数传递给isShorter供isShorter调用。所以说stable_sort调用了myIsShorter,而myIsShorter又调用了stable_sort给它的参数。它们相互调用。这就是“回调”这两个字的含义!
虽然说形参不变,实参可变,以不变应万变。但是作为实参有一点还是不能变的,那就是实参的数据类型不能变。比如void foo(int i)这个函数里的参数i可以取1也可以取2,但是它必须是整型的。同样的,回调函数这种参数的类型也不能变。而函数的类型是由函数的参数类型和返回值类型决定的。比如前面提到的排序算法里面,isShorter这个回调函数的参数必须是两个const string类型,返回值必须是bool类型。所以在写回调函数时还是不能太任性,必须要查看一下调用该回调函数的函数的声明。
总之,所谓回调函数就是把函数当作参数使用。目的是使程序更加普适(正如活字印刷,把可能会“变”的字一个个分离开来,这样就可以任意组合,重复利用)。一般情况下,一个人的小规模程序用不着这种普适性,除非你想把它做成工具箱(比如游戏引擎),供他人使用。
其实实现这种普适性还有其他方法,比如对虚函数进行重写(或者用纯虚函数。Objective C里面所有函数都是虚函数,而协议相当于纯虚函数)。这样同一个函数就可以有不同的实现。不同的合作者之间就可以通过这种虚函数“协议”进行合作。
出处2: https://www.cnblogs.com/jacklikedogs/p/3748042.html
附另一位大牛示例及见解:
//客户端
#include <stdio.h>
int max(int x , int y);
int min(int x , int y);
int add(int x , int y);
int process(int x , int y , int(*fun)(int , int));
void main()
{
int a=10,b=2;
printf("a=%d, b=%d\n", a, b);
printf("process(a,b,max)=%d\n", process(a,b,max));//注册回调函数
printf("process(a,b,min)=%d\n", process(a,b,min));
printf("process(a,b,add)=%d\n", process(a,b,add));
}
int max(int x , int y)
{
return x>y?x:y;
}
int min(int x , int y)
{
return x<y?x:y;
}
int add(int x, int y)
{
return x + y;
}
//服务端
int process(int x, int y, int(* fun)(int, int))
{
int result;
result = (*fun)(x , y);
return result;
}
以上代码,客户端所声明的三个功能函数:max ,min ,add 就是回调函数。
使用回调函数实际上就是在调用某个函数时将自己的一个函数(这个函数就是回调函数)的地址作为参数传递给那个函数。而那个函数在需要的时候,利用传递的地址调用回调函数,这时你可以利用这个机会,在回调函数中处理消息或完成一定的操作。
也可以这样理解:
所谓回调,就是客户程序Client(main)调用服务程序Server中的某个函数Sa(process),然后Server又在某个时候反过来调用Client中的某个函数Ca(max),对于Client来说,这个Ca便叫做回调函数。例如Win32下的窗口过程函数就是一个典型的回调函数。
一般说来,Client不会自己调用Ca,Client提供Ca的目的就是让Server来调用它,而且是Client不得不提供。由于Server并不知道Client提供的Ca叫什么,所以Server会约定Ca的接口规范(函数原 型),然后由Client提前通过Server的一个函数Sr(process)告诉Server,自己将要使用Ca函数,这个过程称为回调函数的注册,Sr称为注册函数。
这个Client的Ca函数是不是很像个钩子?一旦将它注册到Server后,Server就会在合适的场合来调用它了。
下面举个通俗的例子:
假设公司里面有一个小神童,可以回答任何人的问题。
某天,我向小神童请教问题,当然是个难题,:),小神童一时想不出解决方法,而后面还有一帮人正在等着神童回答他们自己的问题呢,
现在不能因为我的提问,而影响到后面排队的人的咨询,所以我和小神童约定,我把手机号码留给他,等小神童知道答案了然后再把结果告诉我。
我就离开小神童办其它事情去了。过了XX分钟,我的手机响了,小神童兴高采烈的说问题已经搞定,应该如此这般处理。故事到此结束。
说明:我是Client,小神童是Server,我找小神童办事就是调用函数Sa,由于某种原因不能马上得到结果,又不能占用大家的时间,所以我只能先注册(Sr函数)联系方式给小神童, 等小神童有消息了,就打电话给我。
这个例子说明了“异步+回调”的编程模式。其中,你后来打手机告诉我结果便是一个“回调”过程;我的手机号码必须在以前告诉你,这便是注册回调函数;我的手机号码应该有效并且手机能够接收到你的呼叫,这是回调函数必须符合接口规范。
2. 什么情况下使用回调
如果你是SDK的使用者,一旦别人制定了回调机制,那么你被迫得使用回调函数,因此这个问题只对SDK设计者有意义。
从引入的目的看,回调大致分为三种:
1) SDK有消息需要通知应用程序,比如定时器被触发;
2) SDK的执行需要应用程序的参与,比如SDK需要你提供一种排序算法;
3) SDK的操作比较费时,但又不能让应用程序阻塞在那里,于是采用异步方式,让调用函数及时返回,SDK另起线程在后台执行操作,待操作完成后再将结果通知应用程序。