回调函数是基于C编程的Windows SDK的技术,不是针对C++的,程序员可以将一个C函数直接作为回调函数,但是如果试图直接使用C++的成员函数作为回调函数将发生错误,甚至编译就不能通过。
普通的C++成员函数都隐含了一个传递函数作为参数,亦即“this”指针,C++通过传递一个指向自身的指针给其成员函数从而实现程序函数可以访问C++的数据成员。这也可以理解为什么C++类的多个实例可以共享成员函数但是确有不同的数据成员。由于this指针的作用,使得将一个CALLBACK型的成员函数作为回调函数安装时就会因为隐含的this指针使得函数参数个数不匹配,从而导致回调函数安装失败。
这样从理论上讲,C++类的成员函数是不能当作回调函数的。但我们在用C++编程时总希望在类内实现其功能,即要保持封装性,如果把回调函数写作普通函数有诸多不便。经过网上搜索和自己研究,发现了几种巧妙的方法,可以使得类成员函数当作回调函数使用。
这里采用Linux C++中线程创建函数pthread_create举例,其原型如下:
int pthread_create( pthread_t *restrict tidp , const pthread_attr_t *restrict attr , void* (*start_rtn)(void*) , void *restrict arg );
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址,即回调函数。
最后一个参数是运行函数的参数。
这里我们只关注第三个参数start_run,它是一个函数指针,指向一个以void*为参数,返回值为void*的函数,这个函数被当作线程的回调函数使用,线程启动后便会执行该函数的代码。
方法一:回调函数为普通函数,但在函数体内执行成员函数
见以下代码:
class MyClass
{
pthread_t TID;
public:
void func()
{
//子线程执行代码
}
bool startThread()
{//启动子线程
int ret = pthread_create( &TID , NULL , callback , this );
if( ret != 0 )
return false;
else
return true;
}
};
static void* callback( void* arg )
{//回调函数
((MyClass*)arg)->func();调用成员函数
return NULL;
}
int main()
{
MyClass a;
a.startThread();
}
类MyClass需要在自己内部开辟一个子线程来执行成员函数func()中的代码,子线程通过调用startThread()成员函数来启动。这里将回调函数callback写在了类外面,传递的参数是一个指向MyClass对象的指针(在pthrad_create()中由第4个参数this指定),回调函数经过强制转换把void*变为MyClass*,然后再调用arg->func()执行子线程的代码。
这样做的原理是把当前对象的指针当作参数先交给一个外部函数,再由外部函数调用类成员函数,以外部函数作为回调函数,但执行的是成员函数的功能,这样相当于在中间作了一层转换。缺点是回调函数在类外,影响了封装性,这里把callback()限定为static,防止在其它文件中调用此函数。
方法二:回调函数为类内静态成员函数,在其内部调用成员函数
在方法一上稍作更改,把回调函数搬到类MyClass里,这样就保持了封装性。代码如下:
class MyClass
{
static MyClass* CurMy;//存储回调函数调用的对象
static void* callback(void*);//回调函数
pthread_t TID;
void func()
{
//子线程执行代码
}
void setCurMy()
{//设置当前对象为回调函数调用的对象
CurMy = this;
}
public:
bool startThread()
{//启动子线程
setCurMy();
int ret = pthread_create( &TID , NULL , MyClass::callback , NULL );
if( ret != 0 )
return false;
else
return true;
}
};
MyClass* MyClass::CurMy = NULL;
void* MyClass::callback(void*)
{
CurMy->func();
return NULL;
}
int main()
{
MyClass a;
a.startThread();
}
类MyClass有了1个静态数据成员CurMy和1个静态成员函数callback。CurMy用来存储一个对象的指针,充当方法一中回调函数的参数arg。callback当作回调函数,执行CurMy->func()的代码。每次建立线程前先要调用setCurMy()来让CurMy指向当前自己。
这个方法的好处时封装性得到了很好的保护,MyClass对外只公开一个接口startThread(),子线程代码和回调函数都被设为私有,外界不可见。另外没有占用callback的参数,可以从外界传递参数进来。但每个对象启动子线程前一定要注意先调用setCurMy()让CurMy正确的指向自身,否则将为其它对象开启线程,这样很引发很严重的后果。
方法三:对成员函数进行强制转换,当作回调函数
代码如下:
class MyClass
{
pthread_t TID;
void func()
{
//子线程执行代码
}
public:
bool startThread()
{//启动子线程
typedef void* (*FUNC)(void*);//定义FUNC类型是一个指向函数的指针,该函数参数为void*,返回值为void*
FUNC callback = (FUNC)&MyClass::func;//强制转换func()的类型
int ret = pthread_create( &TID , NULL , callback , this );
if( ret != 0 )
return false;
else
return true;
}
};
int main()
{
MyClass a;
a.startThread();
}
这个方法是原理是,MyClass::func最终会转化成 void func(MyClass *this); 也就是说在原第一个参数前插入指向对象本身的this指针。可以利用这个特性写一个非静态类成员方法来直接作为线程回调函数。对编译器而言,void (MyClass::*FUNC1)()和void* (*FUNC)(void*)这两种函数指针虽然看上去很不一样,但他们的最终形式是相同的,因此就可以把成员函数指针强制转换成普通函数的指针来当作回调函数。在建立线程时要把当前对象的指针this当作参数传给回调函数(成员函数func),这样才能知道线程是针对哪个对象建立的。
方法三的封装性比方法二更好,因为不涉及多个对象共用一个静态成员的问题,每个对象可以独立地启动自己的线程而不影响其它对象。
暂时就列出这些方法,以后发现更好的再来补充,over