我以前写线程时要么老老实实照着声明写,要么使用C++类的静态成员函数来作为回调函数,经常会因为线程代码而破坏封装.之前虽然知道类成员函数的展开形式,但从没想过利用过它,昨天看深入ATL时无意中学会了这一招:)
类成员方法是一个比较特殊的函数,它在编译时会被转化成普通函数,比如有TMyClass类:
class TMyClass{
1 | void Func(); |
};
这个TMyClass::Func最终会转化成 void Func(TMyClass *this); 也就是说在原第一个参数前插入指向对象本身的this指针。
我们可以利用这个特性写一个非静态类成员方法来直接作为线程回调函数,先看_beginthread函数的定义:
unsigned long RTLENTRY EXPFUNC beginthread (void (USERENTRY start)(void ),unsigned __stksize, void arg);
其中的第一个参数就是作为线程执行主体的回调函数。它的原型是:void Func(void ),这个void*参数是作为自定义数据传入的。对比一下上面所说的TMyClass::Func的最终形式,它正好可以符合这里的要求。
现在做个实验:
include
include
class TMyClass{
1 | int m_nCount; |
2 | int m_nId; |
public:
01 | TMyClass( int nId, int nCount) |
02 | :m_nId(nId),m_nCount(nCount) |
03 | { |
04 | } |
05 |
06 | void _USERENTRY ThreadProc() // 类成员方法 |
07 | { |
08 | for ( int i= 0 ; i<m_nCount; i++) // 根据m_nCount成员打印一排数字 |
09 | { |
10 | printf( "Class%d : %d/n" ,m_nId,i); |
11 | } |
12 | } |
};
int main(int argc, char* argv[])
{
01 | union { // 联合类,用于转换类成员方法指针到普通函数指针(试过编译器不允许在这两种函数之间强制转换),不知道有没有更好的方法。 |
02 | void (_USERENTRY *ThreadProc)( void *); |
03 | void (_USERENTRY TMyClass::*MemberProc)(); |
04 | } Proc; // 尽管联合里的两种函数类型现在看起来有很大不同,但它们的最终形式是相同的。 |
05 |
06 | TMyClass MyClass1( 1 , 10 ),MyClass2( 2 , 5 ); // 产生两个TMyClass对象 |
07 |
08 | Proc.MemberProc = &TMyClass::ThreadProc; // 转换,Proc.ThreadProc就是对应的普通函数指针了 |
09 |
10 | _beginthread(Proc.ThreadProc, 4096 ,&MyClass1); // 开始线程,这里的Proc.ThreadProc实际上是TMyClass::ThreadProc, 它要的this指针是我们给的&MyClass1。 |
11 | _beginthread(Proc.ThreadProc, 4096 ,&MyClass2); |
12 | system( "pause" ); |
13 | return 0 ; |
}
运行!神奇吧?:-)
其实不止线程回调函数,其实只要是形如Func(void,…)的回调函数都可以用这种方法直接使用类成员方法。(前提是第一个void是自定义数据,也就是说它不能有其它功能)。
回调函数是基于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 );
int pthread_create( pthread_t *restrict tidp , const pthread_attr_t *restrict attr , void* (*start_rtn)(void*) , void *restrict arg );
- 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();
- }
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();
- }
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();
- }
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