回调函数设计方法
引入:
你显示器不亮了,你不知道怎么弄,那你就问在外地干IT的大表哥,你大表哥告诉你修理的方法,然后需要你自己来操作。
你大表哥知道怎么弄,但是自己不去弄,而是由你去弄。
换句话说,你大表哥实现了修理你显示器的方法,但他不会自己去调用,而是由你去调用。那么你大表哥告诉你的修机器的方法就是回调函数。
在这个比喻里,你自己 作为主调方,有实际的需求——修显示器,但是没有方法,求教表哥的时候,表哥给你的方法 就是一个 函数地址,当你按照大表哥的方法执行的时候,就是 执行了一个回调函数了。
在工程设计中,尤其在底层库设计的时候,很多时候,库的开发者并不能预测今后使用这段代码的程序员需要这个函数做具体什么工作,这时候,就需要使用回调设计。
C 和 C++都提供这类回调支持,C 的建议是使用函数指针,回调实现,C++则通过对基类的继承,对基类中虚函数的再设计来实现。不过,根据笔者经验,在这点上,C 的方式比C++方式要轻灵,并且更加灵活。因此,在笔者的工程开发中,一般使用回调函数设计,不太使用虚函数机制。
回调函数其实就是函数指针的应用,在 C 中,一切数据均可以指针化表示,函数本身其实也可以,当我们以正确的构型调用一个函数指针时,其效果和直接调用函数本身,完全一样。
另外,由于现代操作系统的 C 亲密性,很多操作系统级的 api 设计都可以看到回调函数 ,比如我们常见的线程函数,甚至进程本身,其实都是操作系统的回调函数,beginthread 这类启动线程的调用,一般就是把指定的线程函数指针,在系统的线程表中,注册一个新的表项,系统下一轮时间循环,自动会根据这个表项,回调该指针,进而实现应用程序线程对时间片的获取。并且,这个过程,一般都是纯 C 的,和 C++无关。
从某种意义上说,现代并行计算,是建立在 C 的回调模型上的。作为程序员,对于回调函数,应该有很深入的认识,并能熟练应用。
回调函数的设计非常简单,不过,这里面首先要搞清楚两个身份,一个是回调函数的
设计者,一个是使用者,但二者都是程序员。
在后文中,使用 回调模型设计者和使用者来区分这两个身份。
回调模型设计者:
作为回调模型的设计者,首先需要定义一个回调函数构型,因为 C 语言就算再灵活,也需要知道函数原型是怎样的,才能确保使用者是正确调用,避免崩溃。
typedef void(*_APP_INFO_OUT_CALLBACK)(char* szInfo,void* pCallParam);
1、typedef,这是我们显式定义一种新的变量类型,这个变量类型,就是这一个回调函数指针的类型。以后使用这个指针的设计者和使用者,都可以使用_APP_INFO_OUT_CALLBACK 这个变量类型来定义自己的指针变量。
2、本回调函数使用 void 作为返回值,是因为这个特殊应用。其实很多时候,有个约定 ,一般回调函数使用 bool 作为返回值,这在某些循环遍历的场合,当使用者感到自己的数据已经找到,循环无需继续,可以返回个 false,设计者就知道,可以不再循环了。这体现出使用者不是完全被动的接受回调,也可以通过返回值影响回调发起方的逻辑。
3、char* szInfo 这是业务数据,这里不再细说。
4、void* pCallParam,这个非常关键,所有回调函数的设计者,一定要帮助使用者传递一根 void*的指针,并透传到每一次回调调用中。
例子:
创建一个支持回调的 类
classCStultzLowDebug { public: CStultzLowDebug(char* szPathName, char* szAppName, //构造函数传入回调函数和参数,可以是 null _APP_INFO_OUT_CALLBACK pInfoOutCallback=null, void* pInfoOutCallbackParam=null); //保存在对象内部,方便 Debug 等功能函数调用 _APP_INFO_OUT_CALLBACK m_pInfoOutCallback; void* m_pInfoOutCallbackParam; };
构造函数 的具体实现:
CStultzLowDebug::CStultzLowDebug(char* szPathName, char* szAppName, _APP_INFO_OUT_CALLBACK pInfoOutCallback, void* pInfoOutCallbackParam) { m_pInfoOutCallback=pInfoOutCallback;//回调函数指针保存 m_pInfoOutCallbackParam=pInfoOutCallbackParam;//参数指针保存 //… }
设计者 对回调函数 的调用方式
intCStultzLowDebug::Debug2File(char*szFormat,...) { //… if(m_pInfoOutCallback)//标准写法,先判断指针有效性 { m_pInfoOutCallback(szInfoOut,//像函数一样调用 m_pInfoOutCallbackParam);//这里在帮助透传指针 } //… }
总结 回调函数的设计的特点:
1、先定义回调函数原型,顺便定义一个新的指针变量类型。
2、设计者以该回调函数指针变量类型定义新的变量,实现参数传递和数据保存。
3、调用前先检查指针有效性,避免跳到空指针处,造成崩溃。
回调模型使用者
作为使用者来说,如果回调函数设计者均基于上述方法设计,其调用程序设计也可以
形成简单规律和套路。
使用者首先必须以回调函数构型构建一个函数,这就是将来的回调函数实体,设计者
的模块会跳至此处运行。使用者在这个函数内部,直接使用传来的变量 szInfo 即可,这就是每次 Debug 模块输出的字符串。
void ApplicationInfomationOutCallback(char* szInfo,void* pCallParam);
但有一点注意,如果是 C 里面,可以这样直接声明和实现函数即可。但在 C++的类中 ,不能这样直接写。这是由于 C++的编译器,为每一个类成员函数,提供了一个默认的隐含指针 this作为参数,指向本次实例化的对象,其类型就是这个类本身。因此,如下所述,这个函数就不对了。
classCStultzLowDebug { private: voidApplicationInfomationOutCallback(char* szInfo,void* pCallParam); };
此时的回调函数原型,由于是类成员函数,有隐含指针,因此相当于如下原型
voidApplicationInfomationOutCallback( CStultzLowDebug*this,//这是 C++编译器在编译时强行添加的 char* szInfo, void* pCallParam);
这时,我们再和回调函数原型比较,发现多了一个 this 指针。两个函数不是一个构型,函数指针类型不匹配,调用将会失败。
因此,所有的回调函数,一旦写在类里面,必须用 static 修饰为静态成员函数。
classCStultzLowDebug { private: //请注意这里的 static 修饰 static void ApplicationInfomationOutCallback(char* szInfo,void* pCallParam); };
C++规定,对于静态类成员函数,将不提供隐含的 this 指针,因此,函数的编译后本体和书写时的声明完全一样,这样就可以把这个函数作为回调函数。
但这随之带来另外一个问题,就是没有了 this 指针,使用起来很不方便。我们知道 ,C++的面向对象设计中,其对象的核心定义就是“一批数据和针对该数据的所有方法的集合。这是面向对象程序设计的精髓。
一个类的成员函数方法,一般说来,都和这个类实例化的对象所包含的数据密
切相关,程序中需要不断访问本对象的成员变量或其他成员函数,也就需要频繁访问本对象指针 this。
因此 在C++里,我们可以有如下的解决方案,即 传参。而回调函数设计者有义务为使用者透传一根 void* 的参数指针
在 实际使用时,将this指针 作为 参数,传给 回调函数,那么 就可以直接使用this,从而操作 类中的数据了。
另外,如果 有 多个参数 需要 传递,就需要使用 传入结构体 指针的方式。
参考文献:《0 bug C/C++商用工程之道》
版权声明:本文为Stultz-Lee原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。