实际工作中,对于回调函数一直是我不愿意去触碰的东西,一来由于被很多人搞得神秘兮兮的以为是很高深难懂的技术,二来在一般情况下通过互相包含类指针也能够解决问题,所以一直就不想去研究这个东东,最近一个项目中被经理逼着使用了回调函数,切切实实体会到了它的好处,代码中类之间的关系再也不那么错综复杂了,类A想告诉类B一个事情再也不需要在A中定义B的指针了,定义回调函数即可。下面把我理解的回调函数写出来,抛出一块砖,个中滋味要各位看客多实践才能体会出来。
1、基础知识
所谓回调,就是模块A要通过模块B的某个函数b()完成一定的功能,但是函数b()自己无法实现全部功能,需要反过头来调用模块A中的某个函数a()来完成,这个a()就是回调函数。如下图
①约定接口规范。在模块B必须约定接口规范,也就是定义回调函数a()的函数原型
这里回调函数原型的定义最好遵循typedef void (*SCT_XXX)(LPVOID lp, const CBParamStruct& cbNode); SCT_XXX是回调函数名称,lp是回调上下文,CBParamStruct是回调参数,一般由于要回调的参数不止一个,所以定义一个结构体比较方便。
②回调函数的注册。为了让模块B知道自己将要使用的回调函数,必须有一个函数或语句来注册回调函数
注册回调函数的定义遵循void RCF_XXX(SCT_XXX pfn, LPVOID lp); RCF_XXX是注册函数名,pfn是回调函数名称(是指针),lp是回调上下文。一般在A模块初始化完B模块后调用,将A模块中定义的回调函数地址赋值给pfn,lp赋值为this。
③在模块A中要做的事情:
首先将回调函数声明成静态的,static void CF_XXX(LPVOID lp, const CBParamStruct& cbNode); 函数的参数必须与B模块中回调函数原型的参数保持一致。
初始化B模块时,调用注册函数将模块A中声明的回调函数CF_XXX的地址传给pfn,即pfn=CF_XXX;(函数名称CF_XXX其实是个指针,指向回调函数的地址) 。
2、举例
回调函数使用第一个场景:MFC界面编程。有这样一个需求,主界面左侧是一个树形列表,右侧是一个绘图区用来展示左侧列表项的内容,双击绘图区弹出框用来编辑。一般的做法是在绘图区对话框初始化时将主对话框或者树形列表的指针传进来,在绘图区对话框中处理双击事件,在事件出来函数中调用主对话框或树形列表的指针完成更新操作。这样主对话框类和绘图区对话框类之间就出现了互相包含的关系,回调函数这个时候就可以大显身手了,主对话框仅需要包含绘图区对话框的头文件和声明一个绘图区对话框的对象即可。具体做法是:在绘图区对话框中定义回调函数原型和注册回调的函数,并处理鼠标双击事件,在事件函数中发出回调通知。主对话框中按原型定义回调函数,在回调函数中完成树形列表的更新。
回调函数的第二个应用场景:网络编程。 在网络编程中,为了体现模块化,一般把通讯和数据处理划分开来,即通讯模块负责协议定义、数据收发,而数据处理模块只负责对收发的数据进行解析和打包,假如通讯模块开启了一个线程在持续地接收数据,这个时候问题来了,它通过什么手段把数据交到数据处理模块手中呢?每次收到数据,拿到数据处理模块的指针完成相关操作,这样有犯了两个类指针互相指的错误,也破坏了两个模块的独立性。使用回调函数这些问题都迎刃而解了,下面给出部分伪代码: