引入:
你显示器不亮了,你不知道怎么弄,那你就问在外地干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*的指针,并透传到每一次回调调用中。
例子:
创建一个支持回调的 类
class CStultzLowDebug
{
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; //参数指针保存
//…
}
设计者 对回调函数 的调用方式
int CStultzLowDebug::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作为参数,指向本次实例化的对象,其类型就是这个类本身。因此,如下所述,这个函数就不对了。
class
CStultzLowDebug{
private:
void ApplicationInfomationOutCallback(char* szInfo,void* pCallParam);
};
此时的回调函数原型,由于是类成员函数,有隐含指针,因此相当于如下原型
void ApplicationInfomationOutCallback(
CStultzLowDebug* this, //这是 C++编译器在编译时强行添加的
char* szInfo,
void* pCallParam);
这时,我们再和回调函数原型比较,发现多了一个 this 指针。
两个函数不是一个构型,函数指针类型不匹配,调用将会失败。
因此,所有的回调函数,一旦写在类里面,必须用 static 修饰为静态成员函数。
class
CStultzLowDebug{
private:
//请注意这里的 static 修饰
static void ApplicationInfomationOutCallback(
char* szInfo,void* pCallParam);
};
C++规定,对于静态类成员函数,将不提供隐含的 this 指针,因此,函数的编译后本体和书写时的声明完全一样,这样就可以把这个函数作为回调函数。
但这随之带来另外一个问题,就是没有了 this 指针,使用起来很不方便。我们知道 ,
C++的面向对象设计中,其对象的核心定义就是“一批数据和针对该数据的所有方法的集
合。”,这是面向对象程序设计的精髓。
因此,一个类的成员函数方法,一般说来,都和这个类实例化的对象所包含的数据密
切相关,程序中需要不断访问本对象的成员变量或其他成员函数,也就需要频繁访问本对象指针 this。
因此 在C++里,我们可以有如下的解决方案,即 传参
回调函数设计者有义务为使用者透传一根 void* 的参数指针
因此 在 实际使用时,将this指针 作为 参数,传给 回调函数,那么 就可以直接使用this
,从而操作 类中的数据了
如果 有 多个参数 需要 传递,就使用 结构体 指针的方式。
参考文献:《0 bug C/C++商用工程之道》