a)将函数指针作为参数,传递给函数,那这个函数指针的函数,就是回调函数。C语言通过函数指针实现,C++通过std::function、std::bind实现,或者仿函数Functor、或者Lambda。
b)比如Sort(nums.begin(), nums.end(), greater)。那么这个greater就是函数指针,我们手动(client侧)实现,传递进入Sort(server侧),然后再在里面调用,作为判断依据,影响Sort的具体排序。可以让Sort更加灵活,只需在client侧修改,无需在server侧修改,尤其是server是库的时候。
c)我(client)去店里(server)买东西,店家告诉我没有,我将电话(自己实现的回调函数,注册给到传入店家)留给店家,店家在到货的时候,打电话通知我。(某一时刻调用我传入的回调函数)。
d)OpenGL程序,我(client)通过glxxx传入MouseMovementCallBack,给到windows窗口系统程序(server),当鼠标移动时,windows窗口程序调用MouseMovementCallBack,使得鼠标位置得以正确调整。
e)Android Listener;Android Camera App/Hal Buffer;YuvSiq客户(client)传递成员函数作为回调函数,往往需要记录一些状态,当我(server)某时调用时,客户就知道了。
函数指针的调用,即是一个通过函数指针调用的函数。
如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就说这是回调函数。
In computer programming, a callback is any executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at a given time. This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback.
即:把一段可执行的代码像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,就叫做回调。如果代码立即被执行就称为同步回调,如果在之后晚点的某个时间再执行,则称为异步回调。
比如:我们去“新白鹿”餐馆点餐,好多人排队正在等餐,你吃完了我才能进去吃,我就在哪儿一直等着......我也不急么;后来你过来要吃饭,我先给你一个电子牌替你排好队,我先做给其他顾客吃,你去干你自己的事(逛附件商场),等好了,我叫你(并把你要的饭菜给你),这就是回调。
回到函数作用:“解耦”,普通函数代替不了回调函数的这个特点。这是回调函数最大的特点。
使用回调函数,和普通函数调用区别:
-
1)在主入口程序中,把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,且不需要修改库函数的实现,变的很灵活,这就是解耦。
-
2)主函数和回调函数是在同一层的,而库函数在另外一层。如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数了,这也就是在日常工作中常见的情况。
回调函数其实就是函数指针的一种用法:A "callback" is any function that is called by another function which takes the first function as a parameter。
注:使用回调函数会有间接调用,因此,会有一些额外的传参与访存开销,对于MCU代码中对时间要求较高的代码要慎用。
#include<stdio.h>
#include<freeLib.h>
// Callback Function
int Callback()
{
// TODO
func();
return 0;
}
// Main program
int main()
{
// TODO
Library(Callback); // Callback为回调函数,以函数指针的形式传入。
return 0;
}
回调函数的使用是对函数指针的应用,函数指针的概念本身很简单,但是把函数指针应用于回调函数就体现了一种解决问题的策略,一种设计系统的思想。
回调函数的缺点:
- 1)回调函数固然能解决一部分系统架构问题但是绝不能再系统内到处都是,如果你发现你的系统内到处都是回调函数,那么你一定要重构你的系统。
- 2)回调函数本身是一种破坏系统结构的设计思路,回调函数会绝对的变化系统的运行轨迹,执行顺序,调用顺序。回调函数的出现会让读到你的代码的人非常的懵头转向。
回调函数是一种不得以而为之的设计策略比如:
在一个下载系统中有一个文件下载模块和一个下载文件当前进度显示模块,系统要求实时的显示文件的下载进度,想想很简单在面向对象的世界里无非是实现两个类而已。但是问题恰恰出在这里,显示模块如何驱动下载进度条?显示模块不知道也不应该知道下载模块所知道的文件下载进度(面向对象设计的封装性,模块间要解耦,模块内要内聚),文件下载进度是只有下载模块才知道的事情,解决方案很简单给下载模块传递一个函数指针作为回调函数驱动显示模块的显示进度。
在面向对象的世界中这样的例子还真不少,造成这样的问题的根源,是面向对象的程序设计思想,设计模式中要求的模块独立性,高内聚低耦合等特性。
我们需要模块间的协作,同时我们又厌恶的摒弃模块间你中有我我中有你的暧昧关系那如何生成系统呢?是函数指针(不一定一定是函数指针)也就是使用回调的方式。如果一个对象关心另一个对象的状态变化那么给状态的变化注册回调函数让它通知你这类状态的改变,这样在封装了模块变化的同时实现了模块间的协作关系另辟独径的给对象解耦。
目前根据看到的资料总结,有以下两个例子,主要都是对函数指针的使用:
1)下述讲解,这也是C++ Primer的解释,sort(it.begin(), it.end(), fun),其中fun为回调函数(自定义写法),因为sort调用了fun,而fun里又调用了sort中前两个参数(即需要排序的数组),因此取名“回调”,看起来这像是同步回调。
2)另一种可能是异步回调,就是常见的说法:当小程去商店买东西时,店家回应没有,然后小程就回去了,店家有货时就打电话通知了小程,此为回调。小程去店里,把电话号码告知店家的过程,应该就表示将回调函数的指针传给它,店家调用了回调函数,就“通知”了小程。上述“通知”,往往是一种行为,比如将数组排序好,比如做一些其他事,可以使立即做(同步),可以是延迟做(异步)。
3)同上,Camera app层下发request/result到Hal层(注册回调函数指针),当sensor出图完成并经过Hal层处理后,Hal层调用回调函数,就“通知”了app层,将request/result取回。
在C/C++里面,函数的概念很好理解,就是把某个任务独立出来,封装在一起,然后给它取个名字,它可以有参数和返回值。那么,回调函数是个什么鬼呢?它和函数到底有何异同?既然已经有了函数,为啥还非要生出个回调函数来?想必,小伙伴们在刚碰到这个概念的时候,都会被这些问题困扰。网上搜一搜,有很多相关的材料,但是未必透彻。我觉得要真正理解一个概念,必须要先理解它存在的意义,也就是它为什么要存在,它能带来什么方便之处。在这一点上C++ Primer这本书写的还是比较到位的。仔细阅读之后,我把自己的心得写下来,供大家参考。
首先,回调函数也是函数,就像白马也是马一样。它具有函数的所有特征,它可以有参数和返回值。其实,单独给出一个函数是看不出来它是不是回调函数的。回调函数区别于普通函数在于它的调用方式。只有当某个函数(更确切的说是函数的指针)被作为参数,被另一个函数调用时,它才是回调函数。就像给你一碗饭,你并不能说它是中饭还是晚饭一样,只有当你在某个时候把它吃掉了你才明确它是中饭还是晚饭(这个比喻貌似有点挫。领会精神就好,哈哈)。
那么问题来了,为什么我们要把函数作为参数来调用呢,直接在函数体里面调用不好吗?这个问题问的好。在这个意义上,“把函数做成参数”和“把变量做成参数”目的是一致的,就是以不变应万变。形参是不变的,而实参是变的。唯一不同的是,普通的实参可以由计算机程序自动产生,而函数这种参数计算机程序是无法自己写出来的,因为函数本身就是程序(要是程序可以写程序的话那就是超级人工智能了),它必须由人来写。所以对于回调函数这种参数而言,它的“变”在于人有变或者人的需求有变。
C++ Primer里面举了个例子就是排序算法。为了使排序算法适应不同类型的数据,并且能够按各种要求进行排序,机智的人类把排序算法做成了一个模版(在标准模版库STL里),并且把判断两个数据之间的“大小”(也可以是“字节数”,或者其他某种可以比较的属性)这个任务(即函数)当成一个参数放在排序算法这个函数的参数列表里,而把它的具体实现就交给了使用排序算法的人。这个判断大小的函数就是一个回调函数。比如我们要给某个vector容器里面的单词进行排序,我们就可以声明一个排序算法:
void stable_sort(vector<string>::iterator iterBegin, vector<string>::iterator iterEnd,
bool (*isShorter)(const 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类型。所以在写回调函数时还是不能太任性,必须要查看一下调用该回调函数的函数的声明。[这里插播一段广告。假如回调函数本应该只有两个参数的,但是我想让它更普适一点,我想传三个参数给它,那怎么办呢?为了解决这个问题,C++标委又发明了lambda表达式和bind函数这两种方法。欲知详情请戳这里和这里。]
总之,所谓回调函数就是把函数当作参数使用。目的是使程序更加普适(正如活字印刷,把可能会“变”的字一个个分离开来,这样就可以任意组合,重复利用)。一般情况下,一个人的小规模程序用不着这种普适性,除非你想把它做成工具箱(比如游戏引擎),供他人使用。