回调函数中如何调用类中的非静态成员变量或非静态成员函数

论坛 期权论坛 脚本     
匿名技术用户   2020-12-27 08:38   11   0

申明:本文非笔者原创,原文转载自:http://blog.csdn.net/bzhxuexi/article/details/19831667

【问题1】如何在类中封装回调函数?

【答】:
a.回调函数只能是全局的或是静态的。
b.全局函数会破坏类的封装性,故不予采用。
c.静态函数只能访问类的静态成员,不能访问类中非静态成员。

【问题2】如何让静态函数访问类的非静态成员?
【解决方案】:

声明一静态函数a(),将类实例对象指针做为参数传入。如:
class A()
{
static void a(A *); //静态函数
void b(); //非静态函数
}
void A::a(A * pThis)
{
pThis->b(); //静态函数中调用非静态函数
}

【问题3】回调函数中如何访问非静态成员?
由于回调函数往往有固定定义,并不接受 A * pThis 参数
如:CALLBACK MyTimerProc(HWND hwnd,UINT uMsg,UINT idEvent,DWORD dwTime);

【解决方案1】:本方案当遇到有多个类实例对象时会有问题。原因是pThis指针只能指向一个对象。
class A()
{
static void a(); //静态回调函数
void b(); //非静态函数
static A * pThis; //静态对象指针
}

A * A::pThis=NULL;
A::A() //构造函数中将this指针赋给pThis,使得回调函数能通过pThis指针访问本对象
{
pThis=this;
}
void A::a()
{
if (pThis==NULL) return;
pThis->b(); //回调函数中调用非静态函数
}

【解决方案2】:本方案解决多个类实例对象时方案1的问题。用映射表存所有对象地址,每个对象保存自己的ID号。
typedef CMap<UINT,UINT,A*,A*> CAMap;
class A()
{
static void a(); //静态回调函数
void b(); //非静态函数
int m_ID; //本对象在列表中的ID号
static int m_SID; //静态当前对象ID (需要时,将m_ID赋值给m_SID以起到调用本对象函数的功能)
static CAMap m_Map; //静态对象映射表
}

CAMap A::m_Map;
int A::m_SID=0;


A::A() //构造函数中将this指针赋给pThis,使得回调函数能通过pThis指针访问本对象
{
if(m_Map.IsEmpty())
{
m_ID=1;
}
else
{
m_ID=m_Map.GetCount()+1;
}
m_Map.SetAt( m_ID, this );
}
void A::a()
{
if (m_Map.IsEmpty()) return;
A * pThis=NULL;
if(m_Map.Lookup(m_SID,pThis))
{
pThis->b(); //回调函数中调用非静态函数
};
}

=================================

C++中类成员函数作为回调函数


回调函数是基于C编程的Windows SDK的技术,不是针对C++的,程序员可以将一个C函数直接作为回调函数,但是如果试图直接使用C++的成员函数作为回调函数将发生错误,甚至编译就不能通过。

普通的C++成员函数都隐含了一个传递函数作为参数,亦即“this”指针,C++通过传递一个指向自身的指针给其成员函数从而实现程序函数可以访问C++的数据成员。这也可以理解为什么C++类的多个实例可以共享成员函数但是确有不同的数据成员。由于this指针的作用,使得将一个CALLBACK型的成员函数作为回调函数安装时就会因为隐含的this指针使得函数参数个数不匹配,从而导致回调函数安装失败。

这样从理论上讲,C++类的成员函数是不能当作回调函数的。但我们在用C++编程时总希望在类内实现其功能,即要保持封装性,如果把回调函数写作普通函数有诸多不便。经过网上搜索和自己研究,发现了几种巧妙的方法,可以使得类成员函数当作回调函数使用。

这里采用Linux C++中线程创建函数pthread_create举例,其原型如下:

  1. int pthread_create( pthread_t *restrict tidp , const pthread_attr_t *restrict attr , void* (*start_rtn)(void*) , void *restrict arg );

第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址,即回调函数。
最后一个参数是运行函数的参数。

这里我们只关注第三个参数start_run,它是一个函数指针,指向一个以void*为参数,返回值为void*的函数,这个函数被当作线程的回调函数使用,线程启动后便会执行该函数的代码。

方法一:回调函数为普通函数,但在函数体内执行成员函数
见以下代码:
  1. class MyClass
  2. {
  3. pthread_t TID;
  4. public:
  5. void func()
  6. {
  7. //子线程执行代码
  8. }
  9. bool startThread()
  10. {//启动子线程
  11. int ret = pthread_create( &TID , NULL , callback , this );
  12. if( ret != 0 )
  13. return false;
  14. else
  15. return true;
  16. }
  17. };
  18. static void* callback( void* arg )
  19. {//回调函数
  20. ((MyClass*)arg)->func();调用成员函数
  21. return NULL;
  22. }
  23. int main()
  24. {
  25. MyClass a;
  26. a.startThread();
  27. }

类MyClass需要在自己内部开辟一个子线程来执行成员函数func()中的代码,子线程通过调用startThread()成员函数来启动。这里将回调函数callback写在了类外面,传递的参数是一个指向MyClass对象的指针(在pthrad_create()中由第4个参数this指定),回调函数经过强制转换把void*变为MyClass*,然后再调用arg->func()执行子线程的代码。

这样做的原理是把当前对象的指针当作参数先交给一个外部函数,再由外部函数调用类成员函数,以外部函数作为回调函数,但执行的是成员函数的功能,这样相当于在中间作了一层转换。缺点是回调函数在类外,影响了封装性,这里把callback()限定为static,防止在其它文件中调用此函数。


方法二:回调函数为类内静态成员函数,在其内部调用成员函数

在方法一上稍作更改,把回调函数搬到类MyClass里,这样就保持了封装性。代码如下:

  1. class MyClass
  2. {
  3. static MyClass* CurMy;//存储回调函数调用的对象
  4. static void* callback(void*);//回调函数
  5. pthread_t TID;
  6. void func()
  7. {
  8. //子线程执行代码
  9. }
  10. void setCurMy()
  11. {//设置当前对象为回调函数调用的对象
  12. CurMy = this;
  13. }
  14. public:
  15. bool startThread()
  16. {//启动子线程
  17. setCurMy();
  18. int ret = pthread_create( &TID , NULL , MyClass::callback , NULL );
  19. if( ret != 0 )
  20. return false;
  21. else
  22. return true;
  23. }
  24. };
  25. MyClass* MyClass::CurMy = NULL;
  26. void* MyClass::callback(void*)
  27. {
  28. CurMy->func();
  29. return NULL;
  30. }
  31. int main()
  32. {
  33. MyClass a;
  34. a.startThread();
  35. }
类MyClass有了1个静态数据成员CurMy和1个静态成员函数callback。CurMy用来存储一个对象的指针,充当方法一中回调函数的参数arg。callback当作回调函数,执行CurMy->func()的代码。每次建立线程前先要调用setCurMy()来让CurMy指向当前自己。

这个方法的好处时封装性得到了很好的保护,MyClass对外只公开一个接口startThread(),子线程代码和回调函数都被设为私有,外界不可见。另外没有占用callback的参数,可以从外界传递参数进来。但每个对象启动子线程前一定要注意先调用setCurMy()让CurMy正确的指向自身,否则将为其它对象开启线程,这样很引发很严重的后果。


方法三:对成员函数进行强制转换,当作回调函数

代码如下:

  1. class MyClass
  2. {
  3. pthread_t TID;
  4. void func()
  5. {
  6. //子线程执行代码
  7. }
  8. public:
  9. bool startThread()
  10. {//启动子线程
  11. typedef void* (*FUNC)(void*);//定义FUNC类型是一个指向函数的指针,该函数参数为void*,返回值为void*
  12. FUNC callback = (FUNC)&MyClass::func;//强制转换func()的类型
  13. int ret = pthread_create( &TID , NULL , callback , this );
  14. if( ret != 0 )
  15. return false;
  16. else
  17. return true;
  18. }
  19. };
  20. int main()
  21. {
  22. MyClass a;
  23. a.startThread();
  24. }
这个方法是原理是,MyClass::func最终会转化成 void func(MyClass *this); 也就是说在原第一个参数前插入指向对象本身的this指针。可以利用这个特性写一个非静态类成员方法来直接作为线程回调函数。对编译器而言,void (MyClass::*FUNC1)()和void* (*FUNC)(void*)这两种函数指针虽然看上去很不一样,但他们的最终形式是相同的,因此就可以把成员函数指针强制转换成普通函数的指针来当作回调函数。在建立线程时要把当前对象的指针this当作参数传给回调函数(成员函数func),这样才能知道线程是针对哪个对象建立的。

方法三的封装性比方法二更好,因为不涉及多个对象共用一个静态成员的问题,每个对象可以独立地启动自己的线程而不影响其它对象。



分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:7942463
帖子:1588486
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP