20161205

简介

  对于很多初学者来说,往往觉得回调函数很神秘,很想知道回调函数的工作原理。本文将要解释什么是回调函数、它们有什么好处、为什么要使用它们等等问题,在开始之前,假设你已经熟知了函数指针。

  什么是回调函数?

  简而言之,回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。

  为什么要使用回调函数?

  因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。

  如果想知道回调函数在实际中有什么作用,先假设有这样一种情况,我们要编写一个库,它提供了某些排序算法的实现,如冒泡排序、快速排序、shell排序、shake排序等等,但为使库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,想让库可用于多种数据类型(int、 float、string),此时,该怎么办呢?可以使用函数指针,并进行回调。

  回调可用于通知机制,例如,有时要在程序中设置一个计时器,每到一定时间,程序会得到相应的通知,但通知机制的实现者对我们的程序一无所知。而此时,就需有一个特定原型的函数指针,用这个指针来进行回调,来通知我们的程序事件已经发生。实际上,SetTimer() API使用了一个回调函数来通知计时器,而且,万一没有提供回调函数,它还会把一个消息发往程序的消息队列。

  另一个使用回调机制的 API函数是EnumWindow(),它枚举屏幕上所有的顶层窗口,为每个窗口调用一个程序提供的函数,并传递窗口的处理程序。如果被调用者返回一个值,就继续进行迭代,否则,退出。EnumWindow()并不关心被调用者在何处,也不关心被调用者用它传递的处理程序做了什么,它只关心返回值,因为基于返回值,它将继续执行或退出。

  不管怎么说,回调函数是继续自C语言的,因而,在C++中,应只在与C代码建立接口,或与已有的回调接口打交道时,才使用回调函数。除了上述情况,在C++中应使用虚拟方法或函数符(functor),而不是回调函数。

  一个简单的回调函数实现

  下面创建了一个sort.dll的动态链接库,它导出了一个名为CompareFunction的类型—

[cpp]  view plain  copy
  1. typedef int (__stdcall *CompareFunction)(const byte*, const byte*)  

它就是回调函数的类型。另外,它也导出了两个方法:Bubblesort()和Quicksort(),这两个方法原型相同,但实现了不同的排序算法。

[cpp]  view plain  copy
  1. void DLLDIR __stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunction cmpFunc);  
  2. void DLLDIR __stdcall Quicksort(byte* array,int size,int elem_size,CompareFunction cmpFunc);  

  这两个函数接受以下参数:

  ·byte * array:指向元素数组的指针(任意类型)。

  ·int size:数组中元素的个数。

  ·int elem_size:数组中一个元素的大小,以字节为单位。

  ·CompareFunction cmpFunc:带有上述原型的指向回调函数的指针。

  这两个函数的会对数组进行某种排序,但每次都需决定两个元素哪个排在前面,而函数中有一个回调函数,其地址是作为一个参数传递进来的。对编写者来说,不必介意函数在何处实现,或它怎样被实现的,所需在意的只是两个用于比较的元素的地址,并返回以下的某个值(库的编写者和使用者都必须遵守这个约定):

  ·-1:如果第一个元素较小,那它在已排序好的数组中,应该排在第二个元素前面。

  ·0:如果两个元素相等,那么它们的相对位置并不重要,在已排序好的数组中,谁在前面都无所谓。

  ·1:如果第一个元素较大,那在已排序好的数组中,它应该排第二个元素后面。

  基于以上约定,函数Bubblesort()的实现如下,Quicksort()就稍微复杂一点:

[cpp]  view plain  copy
  1. void DLLDIR __stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunction cmpFunc)  
  2. {  
  3.     for(int i=0; i < size; i++)  
  4.     {  
  5.         for(int j=0; j < size-1; j++)  
  6.         {  
  7.             /* 回调比较函数 */  
  8.             if(1 == (*cmpFunc)(array+j*elem_size,array+(j+1)*elem_size))  
  9.             {  
  10.                 /* 两个相比较的元素相交换 */  
  11.                 byte* temp = new byte[elem_size];  
  12.                 memcpy(temp, array+j*elem_size, elem_size);  
  13.                 memcpy(array+j*elem_size,array+(j+1)*elem_size,elem_size);  
  14.                 memcpy(array+(j+1)*elem_size, temp, elem_size);  
  15.                 delete [] temp;  
  16.             }  
  17.         }  
  18.     }  
  19. }  

  注意:因为实现中使用了memcpy(),所以函数在使用的数据类型方面,会有所局限。

  对使用者来说,必须有一个回调函数,其地址要传递给Bubblesort()函数。下面有二个简单的示例,一个比较两个整数,而另一个比较两个字符串:

[cpp]  view plain  copy
  1. int __stdcall CompareInts(const byte* velem1, const byte* velem2)  
  2. {  
  3.    int elem1 = *(int*)velem1;  
  4.    int elem2 = *(int*)velem2;  
  5.     
  6.    if(elem1 < elem2)  
  7.     return -1;  
  8.    if(elem1 > elem2)  
  9.     return 1;  
  10.     
  11.    return 0;  
  12. }  

[cpp]  view plain  copy
  1. int __stdcall CompareStrings(const byte* velem1, const byte* velem2)  
  2. {  
  3.     const char* elem1 = (char*)velem1;  
  4.     const char* elem2 = (char*)velem2;  
  5.     return strcmp(elem1, elem2);  
  6. }  

下面另有一个程序,用于测试以上所有的代码,它传递了一个有5个元素的数组给Bubblesort()和Quicksort(),同时还传递了一个指向回调函数的指针。

[cpp]  view plain  copy
  1. int main(int argc, char* argv[])  
  2. {  
  3.    int i;  
  4.    int array[] = {5432, 4321, 3210, 2109, 1098};  
  5.     
  6.    cout << "Before sorting ints with Bubblesort\n";  
  7.    for(i=0; i < 5; i++)  
  8.       cout << array[i] << '\n';  
  9.     
  10.    Bubblesort((byte*)array, 5, sizeof(array[0]), &CompareInts);  
  11.     
  12.    cout <<"After the sorting\n";  
  13.    for(i=0; i < 5; i++)  
  14.       cout << array[i] << '\n';  
  15.     
  16.    const char str[5][10] = {"estella","danielle","crissy","bo","angie"};  
  17.     
  18.    cout << "Before sorting strings with Quicksort\n";  
  19.    for(i=0; i < 5; i++)  
  20.       cout << str[i] << '\n';  
  21.     
  22.    Quicksort((byte*)str, 5, 10, &CompareStrings);  
  23.     
  24.    cout << "After the sorting\n";  
  25.    for(i=0; i < 5; i++)  
  26.       cout << str[i] << '\n';  
  27.     
  28.    return 0;  
  29. }  


  如果想进行降序排序(大元素在先),就只需修改回调函数的代码,或使用另一个回调函数,这样编程起来灵活性就比较大了。

调用约定

  上面的代码中,可在函数原型中找到stdcall,因为它以双下划线打头,所以它是一个特定于编译器的扩展,说到底也就是微软的实现。任何支持开发基于Win32的程序都必须支持这个扩展或其等价物。
stdcall 标识的函数使用了标准调用约定,为什么叫标准约定呢,因为所有的Win32 API(除了个别接受可变参数的除外)都使用它。标准调用约定的函数在它们返回到调用者之前,都会从堆栈中移除掉参数,这也是Pascal的标准约定。但在C/C++中,调用约定是调用者负责清理堆栈,而不是被调用函数;为强制函数使用C/C++调用约定,可使用__cdecl。另外,可变参数函数也使用 C/C++调用约定。

  Windows操作系统采用了标准调用约定(Pascal约定),因为其可减小代码的体积。这点对早期的Windows来说非常重要,因为那时它运行在只有640KB内存的电脑上。

  如果你不喜欢__stdcall,还可以使用CALLBACK宏,它定义在windef.h中:

[cpp]  view plain  copy
  1. #define CALLBACK __stdcallor  
  2. #define CALLBACK PASCAL //而PASCAL在此被#defined成__stdcall  

  作为回调函数的C++方法

  因为平时很可能会使用到C++编写代码,也许会想到把回调函数写成类中的一个方法,但先来看看以下的代码:

[cpp]  view plain  copy
  1. class CCallbackTester  
  2. {  
  3.     public:  
  4.     int CALLBACK CompareInts(const byte* velem1, const byte* velem2);  
  5. };  

Bubblesort((byte*)array, 5, sizeof(array[0]),
&CCallbackTester::CompareInts);

  如果使用微软的编译器,将会得到下面这个编译错误:

error C2664: 'Bubblesort' : cannot convert parameter 4 from 'int (stdcall CCallbackTester::*)(const unsigned char *,const unsigned char *)' to 'int (stdcall *)(const unsigned char *,const unsigned char *)' There is no context in which this conversion is possible

  这是因为非静态成员函数有一个额外的参数:this指针,这将迫使你在成员函数前面加上static。当然,还有几种方法可以解决这个问题,但限于篇幅,就不再论述了。

回调函数:

回调函数是这样一种机制:调用者在初始化一个对象(这里的对象是泛指,包括OOP中的对象、全局函数等)时,将一些参数传递给对象,同时将一个调用者可以访问的函数地址传递给该对象。这个函数就是调用者和被调用者之间的一种通知约定,当约定的事件发生时,被调用者(一般会包含一个工作线程)就会按照回调函数地址调用该函数。

这种方式,调用者在一个线程,被调用者在另一个线程。

在Windows API中有一些函数使用回调函数,例如CreateThread、SetWindowLong等。对应的回调函数定义为如下形式:

function CallBackFunc(Wnd: HWND; Msg, WParam, LParam: Longint): Longint;stdcall;

procedure ThreadFunction(Ptr: Pointer);stdcall;

消息:

消息也可以看作是某种形式的回调,因为消息也是在初始化时由调用者向被调用者传递一个句柄和一个消息编号,在约定的事件发生时被调用者向调用者发送消息。

这种方式,调用者在主线程中,被调用者在主线程或者工作线程中。

Delphi事件模型:

在Delphi的VCL中有很多可视化组件都是使用事件模型,例如TForm的OnCreate事件,其原理是:在设计时指定事件函数,在运行时事件触发,则会调用在设计时指定的事件函数。

在机制上,Delphi事件模型与回调是一样的。但具体形式有些区别,纯的回调函数是全局函数的形式,而Delphi事件是对象方法的形式,即可以定义如下回调函数类型

type

TCallBackFunc = procedure (pData: Pointer) of object;

这就使得Delphi事件只能在Delphi内部使用,而回调函数是可以跨语言的。

另外还要注意,回调函数一般(在Delphi的组件是在主线程中)是在回调体中创建的线程,因此要有临界区保护。

回调函数是一个程序员不能显式调用的函数;通过将回调函数的地址传给调用者从而实现调用。回调函数使用是必要的,在我们想通过一个统一接口实现不同的内容,这时用回掉函数非常合适。比如,我们为几个不同的设备分别写了不同的显示函数:void tvshow(); void computershow(); void notebookshow()…等等。这是我们想用一个统一的显示函数,我们这时就可以用回掉函数了。void show(void (*ptr)()); 使用时根据所传入的参数不同而调用不同的回调函数。

不同的编程语言可能有不同的语法,下面举一个c语言中回调函数的例子,其中一个回调函数不带参数,另一个回调函数带参数。

例子1:

[cpp]  view plain  copy
  1. //test.c  
  2.   
  3. #include <stdlib.h>  
  4. #include <stdio.h>  
  5.   
  6. int test1()  
  7. {  
  8.     int i;  
  9.     for (i=0; i<30; i++)  
  10.     {  
  11.         printf("the %d th charactor is: %c\n", i, (char)('a' + i%26));  
  12.       
  13.     }  
  14.     return 0;  
  15. }  
  16. int test2(int num)  
  17. {  
  18.     int i;  
  19.     for (i=0; i<num; i++)  
  20.     {  
  21.         printf("the %d th charactor is: %c\n", i, (char)('a' + i%26));  
  22.       
  23.     }  
  24.     return 0;  
  25. }  

[cpp]  view plain  copy
  1. void caller1(void (*ptr)())指向函数的指针作函数参数  
  2. {  
  3.     (*ptr)();  
  4. }  
  5. void caller2(int n, int (*ptr)())指向函数的指针作函数参数,这里第一个参数是为指向函数的指针服务的,  
  6. //不能写成void caller2(int (*ptr)(int n)),这样的定义语法错误。  
  7.     (*ptr)(n);  
  8.     return;  
  9. }  

[cpp]  view plain  copy
  1. int main()  
  2. {  
  3.       
  4.     printf("******\n");  
  5.     caller1(test1); //相当于调用test1();  
  6.     printf("&&&&&&******\n");  
  7.     caller2(30, test2); //相当于调用test2(30);  
  8.     return 0;  
  9. }  


以上通过将回调函数的地址传给调用者从而实现调用,但是需要注意的是带参回调函数的用法。要实现回调,必须首先定义函数指针。函数指针的定义这里稍微提一下。比如:

int (*ptr)(); 这里ptr是一个函数指针,其中(*ptr)的括号不能省略,因为括号的优先级高于星号,那样就成了一个返回类型为整型的函数声明了。

使用回调函数实际上就是在调用某个函数(通常是API函数)时,将自己的一个函数(这个函数为回调函数)的地址作为参数传递给那个函数。而那个函数在需要的时候,利用传递的地址调用回调函数,这时你可以利用这个机会在回调函数中处理消息或完成一定的操作。至于如何定义回调函数,跟具体使用的API函数有关,一般在帮助中有说明回调函数的参数和返回值等。C++中一般要求在回调函数前加CALLBACK,这主要是说明该函数的调用方式。DialogBox 的回调函数实际上是个窗口过程,用来处理所有消息。其定义为:

[cpp]  view plain  copy
  1. BOOL CALLBACK DialogProc(  
  2.   
  3. HWND hwndDlg, // handle of dialog box  
  4. UINT uMsg, // message  
  5. WPARAM wParam, // first message parameter  
  6. LPARAM lParam // second message parameter  
  7. );  

在Win32 API中有详细说明。一般使用C++ Builder或MFC的往往没有使用SDK编程的经验,建议找一些SDK编程的书看一下,否则很难理解如何使用窗口过程。
至于钩子函数,只是回调函数的一个特例。习惯上把与SetWindowsHookEx函数一起使用的回调函数称为钩子函数。也有人把利用VirtualQueryEx安装的函数称为钩子函数,不过这种叫法不太流行。

frank的意见:
我对回调函数的理解虽然粗浅,但是我觉得会让人更容易理解:回调函数就相当于一个中断处理函数,由系统在符合你设定的条件时自动调用。为此,你需要做三件事:1,声明;2,定义;3,设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用。
声明和定义时应注意:回调函数由系统调用,所以可以认为它属于WINDOWS系统。不要把它当作你的某个类的成员函数。

ping的意见:
frank 说:回调函数属于WINDOWS系统。我觉得不应该说回调函数是属于系统的。应该说是程序把这段代码的触发交由系统来做。而这种做法是WINDOWS提供的处理机制吧,因为消息是系统一手掌握着的,由系统来调用我们的程序对消息的处理部分,这样子会比较方便。不然我们又得花力气去读消息列表了。

简单的话:你来定义,系统调用.系统自有办法.

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

capVideoStreamCallback 这个回调函数,我没有做过,看了一下Help,应该是通过发送消息
WM_CAP_SET_CALLBACK_VIDEOSTREAM,来设置的,或者调用宏capSetCallbackOnVideoStream
来完成的。这样设定之后,系统在进行图像捕捉的过程中,就会自动调用你写的回调函数。

这个回调函数的函数体需要你自已来写,然后在另一函数中调用,即是说:

[cpp]  view plain  copy
  1. LRESULT CALLBACK capVideoStreamCallback(HWND hWnd,LPVIDEOHDR lpVHdr)  
  2. {  
  3. ……..  
  4. }  

//在另一函数中调用它(即以capVideoStreamCallback的地址作为一参数)
[cpp]  view plain  copy
  1. Function(1,……,capVideoStreamCallback,…..);  


这就好像我们用定时器一样,在设置定时器时需要为定时器设置一回调函数:
::SetTimer(m_hWnd,1,1000,(TIMERPROC)TMProc);这里的TMProc就是回调函数

模块A有一个函数foo,它向模块B传递foo的地址,然后在B里面发生某种事件(event)时,通过从A里面传递过来的foo的地址调用foo,通知A发生了什么事情,让A作出相应反应。
那么我们就把foo称为回调函数。

“这个回调函数不是VFW.h中声明的么,“
——那是声明了回调函数原型,是告诉你传递进来的回调函数必须和它定义的原型保持一致。

”为什么要自己写函数体呢?“
——比如在上面模块B里面,它只知道当event发生时,向模块A发出通知,具体怎么回应这个事件,不是B所关心的,也不是B所能预料到的。
你站在A的角度上思考,当然要你自己作出对event的反应,也就是你要自己写函数体。

你如果明白了C++里面的函数指针,就很容易理解回调函数了。

"不知道系统调用后有什么结果,或者我怎么利用这个结果啊"
—-如果你向系统传递一个回调函数地址,那么你的程序就相当于上面我说的模块A,系统就相当于模块B,系统只是调用你的函数,它根本不可能知道会有什么结果。
你怎么利用这个结果,看你是怎么定义这个回调函数的。
回调函数和回调机制是不同的概念,。,,函数是被调用的,但是回调机制在不同的语言中不都是以函数指针来实现的。。。。比如c#…一般的在windows api 中,回调都是使用函数指针实现的。。。

回调函数,我们经常在Windows用到,可到了如今让我自己给别人提供回调函数,着实让我懵了一把。这东西怎么是实现啊?——以前我可是总使用别人的。
什么是回调函数呢?它的原理机制又是怎么样的呢?从英文单词的理解到使用方法的分析,我自己总结了一下,不知是否正确,大家给些想法。

打个通俗的比方:老板给我交代一项任务(我当然是力巴了),让我给A项目做个方案,尽快完成,完成后给他打个电话,并把报价和项目周期告诉给他,他要和用户联系并对项目评估。
这里面有几项要素了:
1、老板[LevelUP]=上层
2、我[LevelLow]=底层
3、A项目[Param]=参数
4、电话[CallBackNumber]=联系方式(Call Back number)
5、报价和项目周期[retData]=返回的数据
6、尽快=没有要求是马上,因此是异步方式
7、和用户联系并对项目评估=老板接到电话之后要做的事情[上层要完成的工作]
那么,回调函数可以理解为:
上层对其下层的调用,回调函数的地址就是下层完成工作之后通知上层联络方式,回调函数的返回参数是下层完成工作之后返回上层的数据,回调函数的实现就是上层接到通知和数据之后自己随后好作的工作。

回调函数使用的场合:
1、异步数据处理
2、上下层之间的数据交换

从层次关系上看:
是上层应用提供给下层应用的“接口”

由于是跨层、跨内存的,因此:
回调函数只能以地址的形式传递,就像电话号码一样(管它是办公室电话、还是手机,反正打这个电话就能找到老板)。

至此,有个问题:
虽然callback函数的codes是由上层实现的,但是底层有没有把callback内容embed到自己的进程中?callback依然是在用户的进中完成?或者是说,底层仅仅给上层一个singal。还是相反?

如果按照上面的类比看,callback的codes应该在上层的内存段运行比较合理一些。或者,callback的codes是在一个share/common memory之中?

希望大家给出一些讨论。

回调函数浅谈
http://www3.ccw.com.cn/club/essence/200202/8259.htm
这是一种类似根据不同条件调用同一个返回值+参数类型一致的不同函数实现的机制.
针对Windows的消息机制一般回调函数指窗口函数, 根据不同消息都使用该窗口函数进行操作.
针对用户自己的应用也可以建立起相应的消息机制, 类似VB里面的事件
1. 声明一个函数指针
2. 提供函数实现的一方在初始化的时候将函数指针初始化到相应的实现函数注册到调用者
3. 在特定事件/条件发生的时候, 调用者使用函数指针进行调用.
这就是回调函数的一般操作模式, 其实你如果学习COM就会发现, 里面的ConnectionPoint也是利用这种方式进行的, 客户端注册自己的事件处理子程序给服务队组件, 组件通过调用这些函数触发事件, 其实回调的精髓就是你规定函数形式(返回值, 参数)我提供实现和具体的调用地址.
回调函数的内在机制
http://www.china-askpro.com/msg25/qa03.shtml
使用回调函数实际上就是在调用某个函数(通常是API函数)时,将自己的一个函数(这个函数为回调函数)的地址作为参数传递给那个函数。而那个函数在需要的时候,利用传递的地址调用回调函数,这时你可以利用这个机会在回调函数中处理消息或完成一定的操作。至于如何定义回调函数,跟具体使用的API函数有关,一般在帮助中有说明回调函数的参数和返回值等。C++中一般要求在回调函数前加CALLBACK,这主要是说明该函数的调用方式。DialogBox的回调函数实际上是个窗口过程,用来处理所有消息。其定义为:
BOOL CALLBACK DialogProc(
HWND hwndDlg, // handle of dialog box
UINT uMsg, // message
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
在Win32 API中有详细说明。一般使用C++ Builder或MFC的往往没有使用SDK编程的经验,建议找一些SDK编程的书看一下,否则很难理解如何使用窗口过程。
至于钩子函数,只是回调函数的一个特例。习惯上把与SetWindowsHookEx函数一起使用的回调函数称为钩子函数。也有人把利用VirtualQueryEx安装的函数称为钩子函数,不过这种叫法不太流行。
声明函数指针并实现回调
http://www.vckbase.com/document/viewdoc/?id=195
异步消息的传递--回调机制
http://www-128.ibm.com/developerworks/cn/linux/l-callback/index.html?ca=dwcn-newsletter-linux
软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。同步调用是三者当中最简单的,而回调又常常是异步调用的基础,因此,下面我们着重讨论回调机制在不同软件架构中的实现。

1 什么是回调

软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。同步调用是三者当中最简单的,而回调又常常是异步调用的基础,因此,下面我们着重讨论回调机制在不同软件架构中的实现。

对于不同类型的语言(如结构化语言和对象语言)、平台(Win32、JDK)或构架(CORBA、DCOM、WebService),客户和服务的交互除了同步方式以外,都需要具备一定的异步通知机制,让服务方(或接口提供方)在某些情况下能够主动通知客户,而回调是实现异步的一个最简捷的途径。

对于一般的结构化语言,可以通过回调函数来实现回调。回调函数也是一个函数或过程,不过它是一个由调用方自己实现,供被调用方使用的特殊函数。

在面向对象的语言中,回调则是通过接口或抽象类来实现的,我们把实现这种接口的类成为回调类,回调类的对象成为回调对象。对于象C++或Object Pascal这些兼容了过程特性的对象语言,不仅提供了回调对象、回调方法等特性,也能兼容过程语言的回调函数机制。

Windows平台的消息机制也可以看作是回调的一种应用,我们通过系统提供的接口注册消息处理函数(即回调函数),从而实现接收、处理消息的目的。由于Windows平台的API是用C语言来构建的,我们可以认为它也是回调函数的一个特例。

对于分布式组件代理体系CORBA,异步处理有多种方式,如回调、事件服务、通知服务等。事件服务和通知服务是CORBA用来处理异步消息的标准服务,他们主要负责消息的处理、派发、维护等工作。对一些简单的异步处理过程,我们可以通过回调机制来实现。

下面我们集中比较具有代表性的语言(C、Object Pascal)和架构(CORBA)来分析回调的实现方式、具体作用等。

2 过程语言中的回调(C)

2.1 函数指针

回调在C语言中是通过函数指针来实现的,通过将回调函数的地址传给被调函数从而实现回调。因此,要实现回调,必须首先定义函数指针,请看下面的例子:

void Func(char *s);// 函数原型void (*pFunc) (char *);//函数指针

可以看出,函数的定义和函数指针的定义非常类似。
一般的化,为了简化函数指针类型的变量定义,提高程序的可读性,我们需要把函数指针类型自定义一下。

typedef void(*pcb)(char *);

回调函数可以象普通函数一样被程序调用,但是只有它被当作参数传递给被调函数时才能称作回调函数。 被调函数的例子:

void GetCallBack(pcb callback){/*do something*/}用户在调用上面的函数时,需要自己实现一个pcb类型的回调函数:void fCallback(char *s) {/* do something */} 然后,就可以直接把fCallback当作一个变量传递给GetCallBack,GetCallBack(fCallback);

如果赋了不同的值给该参数,那么调用者将调用不同地址的函数。赋值可以发生在运行时,这样使你能实现动态绑定。

2.2 参数传递规则

到目前为止,我们只讨论了函数指针及回调而没有去注意ANSI C/C++的编译器规范。许多编译器有几种调用规范。如在Visual C++中,可以在函数类型前加_cdecl,_stdcall或者_pascal来表示其调用规范(默认为_cdecl)。C++ Builder也支持_fastcall调用规范。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。

将调用规范看成是函数类型的一部分是很重要的;不能用不兼容的调用规范将地址赋值给函数指针。例如:

// 被调用函数是以int为参数,以int为返回值stdcall int callee(int); // 调用函数以函数指针为参数void caller( __cdecl int(*ptr)(int)); // 在p中企图存储被调用函数地址的非法操作cdecl int(*p)(int) = callee; // 出错

指针p和callee()的类型不兼容,因为它们有不同的调用规范。因此不能将被调用者的地址赋值给指针p,尽管两者有相同的返回值和参数列

2.3 应用举例

C语言的标准库函数中很多地方就采用了回调函数来让用户定制处理过程。如常用的快速排序函数、二分搜索函数等。
快速排序函数原型:

void qsort(void *base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void *, const void *));二分搜索函数原型:void *bsearch(const void *key, const void *base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void *, const void *));

其中fcmp就是一个回调函数的变量。
下面给出一个具体的例子:

#include <stdio.h>#include <stdlib.h>int sort_function( const void *a, const void *b);int list[5] = { 54, 21, 11, 67, 22 };int main(void){ int x; qsort((void *)list, 5, sizeof(list[0]), sort_function); for (x = 0; x < 5; x++) printf("%i\n", list[x]); return 0;}int sort_function( const void *a, const void *b){ return *(int*)a-*(int*)b;}

轻松使用自己的回调函数
http://www3.ccw.com.cn/club/essence/200310/19288.htm

回调函数是一个很有用,也很重要的概念。当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数。回调函数在windows编程使用的场合很多,比如Hook回调函数:MouseProc,GetMsgProc以及EnumWindows,DrawState的回调函数等等,还有很多系统级的回调过程。本文不准备介绍这些函数和过程,而是谈谈实现自己的回调函数的一些经验。

之所以产生使用回调函数这个想法,是因为现在使用VC和Delphi混合编程,用VC写的一个DLL程序进行一些时间比较长的异步工作,工作完成之后,需要通知使用DLL的应用程序:某些事件已经完成,请处理事件的后续部分。开始想过使用同步对象,文件影射,消息等实现DLL函数到应用程序的通知,后来突然想到可不可以在应用程序端先写一个函数,等需要处理后续事宜的时候,在DLL里直接调用这个函数即可。

于是就动手,写了个回调函数的原形。在VC和 Delphi里都进行了测试

一:声明回调函数类型。
vc版
typedef int (WINAPI *PFCALLBACK)(int Param1,int Param2) ;

Delph版
PFCALLBACK = function(Param1:integer;Param2:integer):integer;stdcall;

实际上是声明了一个返回值为int,传入参数为两个int的指向函数的指针。
由于C++和PASCAL编译器对参数入栈和函数返回的处理有可能不一致,把函数类型用WINAPI(WINAPI宏展开就是__stdcall)或stdcall统一修饰。

二:声明回调函数原形
声明函数原形
vc版
int WINAPI CBFunc(int Param1,int Param2);

Delphi版
function CBFunc(Param1,Param2:integer):integer;stdcall;

以上函数为全局函数,如果要使用一个类里的函数作为回调函数原形,把该类函数声明为静态函数即可。

三: 回调函数调用调用者
调用回调函数的函数我把它放到了DLL里,这是一个很简单的VC生成的WIN32 DLL.并使用DEF文件输出其函数名 TestCallBack。实现如下:

PFCALLBACK gCallBack=0;
void WINAPI TestCallBack(PFCALLBACK Func)
{
if(Func==NULL)return;
gCallBack=Func;
DWORD ThreadID=0;

HANDLE hThread = CreateThread(NULL,NULL,Thread1,LPVOID(0),&ThreadID );
return;
}
此函数的工作把传入的 PFCALLBACK Func参数保存起来等待使用,并且启动一个线程。声明了一个函数指针PFCALLBACK gCallBack保存传入的函数地址。

四: 回调函数如何被使用:
TestCallBack函数被调用后,启动了一个线程,作为演示,线程人为的进行了延时处理,并且把线程运行的过程打印在屏幕上. 本段线程的代码也在DLL工程里实现

ULONG WINAPI Thread1(LPVOID Param)
{

TCHAR Buffer[256];
HDC hDC = GetDC(HWND_DESKTOP);
int Step=1;
MSG Msg;
DWORD StartTick;
//一个延时循环
for(;Step<200;Step++)
{
StartTick = GetTickCount();
/*这一段为线程交出部分运行时间以让系统处理其他事务*/
for(;GetTickCount()-StartTick<10;)
{
if(PeekMessage(&Msg,NULL,0,0,PM_NOREMOVE) )
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
}
/*把运行情况打印到桌面,这是vcbear调试程序时最喜欢干的事情*/
sprintf(Buffer,"Running %04d",Step);
if(hDC!=NULL)
TextOut(hDC,30,50,Buffer,strlen(Buffer));
}

/*延时一段时间后调用回调函数*/
(*gCallback)(Step,1);

/*结束*/
::ReleaseDC (HWND_DESKTOP,hDC);
return 0;
}

五:万事具备
使用vc和Delphi各建立了一个工程,编写回调函数的实现部分
VC版
int WINAPI CBFunc(int Param1,int Param2)
{
int res= Param1+Param2;
TCHAR Buffer[256]="";
sprintf(Buffer,"callback result = %d",res);
MessageBox(NULL,Buffer,"Testing",MB_OK); //演示回调函数被调用
return res;
}

Delphi版
function CBFunc(Param1,Param2:integer):integer;
begin
result:= Param1+Param2;
TForm1.Edit1.Text:=inttostr(result); / /演示回调函数被调用
end;

使用静态连接的方法连接DLL里的出口函数 TestCallBack,在工程里添加 Button( 对于Delphi的工程,还需要在Form1上放一个Edit控件,默认名为Edit1)。
响应ButtonClick事件调用 TestCallBack

TestCallBack(CBFunc) //函数的参数CBFunc为回调函数的地址

函数调用创建线程后立刻返回,应用程序可以同时干别的事情去了。现在可以看到屏幕上不停的显示字符串,表示dll里创建的线程运行正常。一会之后,线程延时部分结束结束,vc的应用程序弹出MessageBox,表示回调函数被调用并显示根据Param1,Param2运算的结果,Delphi的程序 edit控件里的文本则被改写成Param1,Param2 的运算结果。

可见使用回调函数的编程模式,可以根据不同的需求传递不同的回调函数地址,或者定义各种回调函数的原形(同时也需要改变使用回调函数的参数和返回值约定),实现多种回调事件处理,可以使程序的控制灵活多变,也是一种高效率的,清晰的程序模块之间的耦合方式。在一些异步或复杂的程序系统里尤其有用 — 你可以在一个模块(如DLL)里专心实现模块核心的业务流程和技术功能,外围的扩展的功能只给出一个回调函数的接口,通过调用其他模块传递过来的回调函数地址的方式,将后续处理无缝地交给另一个模块,随它按自定义的方式处理。
 
本文的例子使用了在DLL里的多线程延时后调用回调函数的方式,只是为了突出一下回调函数的效果,其实只要是在本进程之内,都可以随你高兴可以把函数地址传递来传递去,当成回调函数使用。
 
这样的编程模式原理非常简单单一:就是把函数也看成一个指针一个地址来调用,没有什么别的复杂的东西,仅仅是编程里的一个小技巧。至于回调函数模式究竟能为你带来多少好处,就看你是否使用,如何使用这种编程模式了。

代码:

回调函数
声明回调函数类型
声明了一个返回值为int,传入参数为两个int的指向函数的指针
typedef int (WINAPI* PFCALLBACK)(int Param1, int Param2);

//声明回调函数原型
int WINAPI CBFunc(int Param1,int Param2);

//回调函数的实现部分
int WINAPI CBFunc(int Param1,int Param2)
{
int res = Param1 + Param2;
TCHAR Buffer[256]="";
sprintf(Buffer,"callback result = %d",res);
MessageBox(NULL,Buffer,"Testing",MB_OK); //演示回调函数被调用
return res;
}


调用函数
PFCALLBACK gCallBack = 0;声明了一个函数指针保存函数地址
void WINAPI TestCallBack(PFCALLBACK Func)
把传入的参数保存起来等待使用,并启动一个线程
{
if (Func == NULL) return;
gCallBack = Func;
DWORD ThreadID = 0;

HANDLE hThread = CreateThread(NULL,NULL,Thread1,LPVOID(0),&ThreadID);
return;
}

ULONG WINAPI Thread1(LPVOID Param)//线程函数
{
TCHAR Buffer[256];
HDC hDC = GetDC(HWND_DESKTOP);
int Step=1;
MSG Msg;
DWORD StartTick;
//一个延时循环
for(;Step<200;Step++)
{
StartTick = GetTickCount();
/*这一段为线程交出部分运行时间以让系统处理其他事务*/
for(;GetTickCount()-StartTick<10;)
{
if(PeekMessage(&Msg,NULL,0,0,PM_NOREMOVE) )
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
}
/*把运行情况打印到桌面,这是vcbear调试程序时最喜欢干的事情*/
sprintf(Buffer,"Running %04d",Step);
if(hDC!=NULL)
TextOut(hDC,30,50,Buffer,strlen(Buffer));
}

/*延时一段时间后调用回调函数*/
(*gCallBack)(Step,1);

/*结束*/
::ReleaseDC (HWND_DESKTOP,hDC);
return 0;
}

程序员常常需要实现回调。本文将讨论函数指针的基本原则并说明如何使用函数指针实现回调。注意这里针对的是普通的函数,不包括完全依赖于不同语法和语义规则的类成员函数(类成员指针将在另文中讨论)。

声明函数指针

回调函数是一个程序员不能显式调用的函数;通过将回调函数的地址传给调用者从而实现调用。要实现回调,必须首先定义函数指针。尽管定义的语法有点不可思议,但如果你熟悉函数声明的一般方法,便会发现函数指针的声明与函数声明非常类似。请看下面的例子:

void f();// 函数原型

上面的语句声明了一个函数,没有输入参数并返回void。那么函数指针的声明方法如下:

void (*) ();

让我们来分析一下,左边圆括弧中的星号是函数指针声明的关键。另外两个元素是函数的返回类型(void)和由边圆括弧中的入口参数(本例中参数是空)。注意本例中还没有创建指针变量-只是声明了变量类型。目前可以用这个变量类型来创建类型定义名及用sizeof表达式获得函数指针的大小:

// 获得函数指针的大小
unsigned psize = sizeof (void (*) ());

// 为函数指针声明类型定义
typedef void (*pfv) ();

pfv是一个函数指针,它指向的函数没有输入参数,返回类行为void。使用这个类型定义名可以隐藏复杂的函数指针语法。

指针变量应该有一个变量名:

void (*p) (); //p是指向某函数的指针

p是指向某函数的指针,该函数无输入参数,返回值的类型为void。左边圆括弧里星号后的就是指针变量名。有了指针变量便可以赋值,值的内容是署名匹配的函数名和返回类型。例如:

void func()
{
/* do something */
}
p = func;

p的赋值可以不同,但一定要是函数的地址,并且署名和返回类型相同。

传递回调函数的地址给调用者

现在可以将p传递给另一个函数(调用者)- caller(),它将调用p指向的函数,而此函数名是未知的:

void caller(void(*ptr)())
{
ptr(); /* 调用ptr指向的函数 */
}
void func();
int main()
{
p = func;
caller(p); /* 传递函数地址到调用者 */
}

如果赋了不同的值给p(不同函数地址),那么调用者将调用不同地址的函数。赋值可以发生在运行时,这样使你能实现动态绑定。

调用规范

到目前为止,我们只讨论了函数指针及回调而没有去注意ANSI C/C++的编译器规范。许多编译器有几种调用规范。如在Visual C++中,可以在函数类型前加_cdecl,_stdcall或者_pascal来表示其调用规范(默认为_cdecl)。C++ Builder也支持_fastcall调用规范。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。

将调用规范看成是函数类型的一部分是很重要的;不能用不兼容的调用规范将地址赋值给函数指针。例如:

// 被调用函数是以int为参数,以int为返回值
__stdcall int callee(int);

// 调用函数以函数指针为参数
void caller( __cdecl int(*ptr)(int));

// 在p中企图存储被调用函数地址的非法操作
__cdecl int(*p)(int) = callee; // 出错

指针p和callee()的类型不兼容,因为它们有不同的调用规范。因此不能将被调用者的地址赋值给指针p,尽管两者有相同的返回值和参数列。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值