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

来源:互联网 发布:微软软件开发流程 编辑:程序博客网 时间:2024/06/03 20:34

一、回调函数

 

在看LWIP时,见到用回调函数,再看某老外公司OPC源代码时,见到用回调函数。看我国内某些代码(我公司软件等)时没用到。于是,我对回调函数产生了很大的好奇。以前,我写VC程序时用到过回调函数,但是没有用C语言来使用。最近,看到国外大量的经典代码中广泛使用了回调函数(LWIP、某两个公司的OPC程序等),都是C语言来实现的,而不是VC windows程序中别人实现自己使用的那种。

为了弄明白这种函数的奥妙,首先提出三个问题:

1.        回调函数是什么东西?

2.        回调函数怎么开发,怎么使用?

3.        回调函数的作用,应该在什么情况下使用?

 

带着问题来学习,有目的!呵呵,个人经验。

打开baidu.com、google.cn搜索了好多资料,如下:

顺便提一下,某君的一个签名很让我佩服:1好好活着,因为我们会死很久。2五千年的文明 两百年的无奈

 

第一个问题:

*******************************************************************************

其实回调就是一种利用函数指针进行函数调用的过程.  

为什么要用回调呢?比如我要写一个子模块给你用,   来接收远程socket发来的命令.当我接收到命令后,   需要调用你的主模块的函数,   来进行相应的处理.但是我不知道你要用哪个函数来处理这个命令,     我也不知道你的主模块是什么.cpp或者.h,   或者说,   我根本不用关心你在主模块里怎么处理它,   也不应该关心用什么函数处理它......   怎么办?

使用回调!

—— lone wolf

 

使用回调函数实际上就是在调用某个函数(通常是API函数)时,将自己的一个函数(这个函数为回调函数)的地址作为参数传递给那个函数。而那个函数在需要的时候,利用传递的地址调用回调函数,这时你可以利用这个机会在回调函数中处理消息或完成一定的操作。

—— 某专家

 

回调函数,就是由你自己写的。你需要调用另外一个函数,而这个函数的其中一个参数,就是你的这个回调函数名。这样,系统在必要的时候,就会调用你写的回调函数,这样你就可以在回调函数里完成你要做的事。

—— 绿叶

 

http://hi.baidu.com/zhuyipeng/blog/item/863fefdb7c736c63d1164eec.html 是一篇比较好的文章。

 

什么是回调函数?
  回调函数是应用程序提供给Windows系统DLL或其它DLL调用的函数,一般用于截获消息、获取系统信息或处理异步事件。应用程序把回调函数的地址指针告诉DLL,而DLL在适当的时候会调用该函数。回调函数必须遵守事先规定好的参数格式和传递方式,否则DLL一调用它就会引起程序或系统的崩溃。通常情况下,回调函数采用标准WindowsAPI的调用方式,即__stdcall,当然,DLL编制者可以自己定义调用方式,但客户程序也必须遵守相同的规定。在__stdcall方式下,函数的参数按从右到左的顺序压入堆栈,除了明确指明是指针或引用外,参数都按值传递,函数返回之前自己负责把参数从堆栈中弹出。
  理解回调函数!

—— jufengfeng

 

Function Pointers provide the concept of callback functions.

—— newty.de

*******************************************************************************

看了这么多的资料,我只将每位的定义总结一下就一句话:回调函数就是函数指针的一种用法。

在部分资料上,大量讨论了回调函数怎么被调用,到底被谁调用,还有好多的图形,我认为都没有看到问题的本质。

 

 

第二个问题:

*********************************************************************

我实现了一个很简单的回调函数。

#include <stdio.h>

 

void printWelcome(int len)

{

       printf("欢迎欢迎 -- %d/n", len);

}

 

void printGoodbye(int len)

{

       printf("送客送客 -- %d/n", len);

}

 

void callback(int times, void (* print)(int))

{

       int i;

       for (i = 0; i < times; ++i)

       {

              print(i);

       }

       printf("/n我不知道你是迎客还是送客!/n/n");

}

void main(void)

{

       callback(10, printWelcome);

       callback(10, printGoodbye);

       printWelcome(5);

}

*******************************************************************************

上面的代码没有被任何系统函数调用,说明那些东西只是撒撒土迷迷路人眼而已。还有面相对象编程时,用class给封装起来也是掩人耳目,不要被外表所迷惑。

 

 

第三个问题:

*********************************************************************

用过STL的人都知道,在STL中众多算法和程序都用到回调函数,这实现了一种策略。只要任何符合我的标准的函数和计算都可以用我这个公式。你可以实现各种各样的回调函数,只要符合我的格式就能用。

就上面的程序来说,你只要函数格式符合cllback第二个参数的格式不论你给别人做饭、铺床叠被都可以正常工作。这就是回调的作用,把回调实现留给别人。

这是一个用法。

 

有一位朋友用分层的概念来解释了回调机制:callback函数为B层,main函数和print*函数为A层,A层调用了B层的回调函数callmeback,而B层的回调函数调用了A层的实现函数print*。说白了B层就是一个接口。

 

 

这是我的理解。Over


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

注:与tr1::function对象结合使用,能获得更好的效果,详情见http://blog.csdn.net/this_capslock/article/details/38564719


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

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

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

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

[cpp] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  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*的函数,这个函数被当作线程的回调函数使用,线程启动后便会执行该函数的代码。

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

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

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


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

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

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

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


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

代码如下:

[cpp] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  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.   
  21. int main()  
  22. {  
  23.     MyClass a;  
  24.     a.startThread();  
  25. }  
这个方法是原理是,MyClass::func最终会转化成 void func(MyClass *this); 也就是说在原第一个参数前插入指向对象本身的this指针。可以利用这个特性写一个非静态类成员方法来直接作为线程回调函数。对编译器而言,void (MyClass::*FUNC1)()和void* (*FUNC)(void*)这两种函数指针虽然看上去很不一样,但他们的最终形式是相同的,因此就可以把成员函数指针强制转换成普通函数的指针来当作回调函数。在建立线程时要把当前对象的指针this当作参数传给回调函数(成员函数func),这样才能知道线程是针对哪个对象建立的。

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


暂时就列出这些方法,以后发现更好的再来补充,over


参考文献:http://blog.csdn.net/callmeback/article/details/4242260/

http://blog.csdn.net/this_capslock/article/details/17001003

0 0
原创粉丝点击