作者: Marius Bancila
翻译: QiuTao****
介绍****
如果你正在读这篇文章,也许你是想知道什么是回调函数。这篇文章讲解了什么是回调函数,他们有什么优点,以及为什么你要使用他们等等。但是在学习回调函数之前,你应该对函数指针有一定得了解。如果你还不了解,可以参考C/C++书籍或者下面的链接:
- The Syntax of C and C++ Function Pointers
- Pointers to member functions
- Declaring, Assigning, and Using Function Pointers
什么是回调函数?****
对这个问题的一个简单回答就是,回调函数是一个通过函数指针调用的函数。如果你传递一个函数指针(地址)参数给另一个函数,当那个指针被用作去调用它所指的函数时,回调就产生了。
为什么要使用回调函数?****
因为他们将调用者和被调者分开了,调用者不需要关心被调者,它仅仅需要知道被调者的原型以及某些可能的限制(例如,返回值可以为int,但是特定的只有特定的意义)
如果你想知道在它在现实中的用途,想象一下你想写一个提供实现排序算法的库,比如冒泡排序,希尔排序,快速排序等等。但是你不想将排序的逻辑(元素之间的先后关系)放在你的函数中,从而使得库更加易于使用。你希望客户自己实现排序之间的逻辑关系,或者你希望可以用于各种各样的数据类型(int,floats,strings等等)。你该怎么办呢?你可以使用函数指针并进行回调。
回调可以被用作通知,例如。你需要在程序中设置一个定时器,每当一段时间过去后,你的程序必须被通知。但是,定时器的实现机制对你的程序一无所知。它仅仅希望一个给定原型的函数指针,并使用函数指针进行回调,通知你的程序某个事件发生了。确实,API函数SetTimer在一段时间到达后使用一个回调进行通知(如果没有提供回调函数,它将给应用程序队列投递一个消息)。
另外一个使用回调函数的API函数EnumWindow,枚举当前屏幕中所有的顶层窗口。EnumWindow迭代每一个顶层窗口,传递一个窗口句柄给提供的回调函数。如果被调者返回一个值,那么迭代继续,否则迭代停止。EnumWindow并不关系被调用者是什么以及它对传读过去的句柄做了些什么,它仅仅对其返回值感兴趣,因为它是基于该返回值决定是否需要继续执行。
然而,回调函数是从C继承而来的,因此,在C++中应该仅仅在需要与C代码中进行交互时使用,除此之外,你应该使用虚拟函数或者函数对象,而不是回调函数。
一个简单的实现例子****
下面的例子可以再附件文件中找到,我创建了一个叫做sort.dll的动态链接库。它导出了一个叫做CompareFunction类型的函数:
typedef int (__stdcall CompareFunction)(const byte, const byte*);
该函数原型是你的回调函数的类型,另外还导出了两个方法,分别是BubbleSort和QuickSort,这两个函数有一样的原型,但是提供了不一样的排序算法。
void DLLDIR stdcall Bubblesort(byte* array,
int size,
int elem_size,
CompareFunction cmpFunc);
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,比这稍微复杂点,可以参见附件):
· void DLLDIR __stdcall Bubblesort(byte array,
· int size,
· int elem_size,
· CompareFunction cmpFunc)
· {
· for(int i=0; i < size; i++)
· {
· for(int j=0; j < size-1; j++)
· {
· // make the callback to the comparison function
· if(1 == (cmpFunc)(array+jelem_size,
· array+(j+1)elem_size))
· {
· // the two compared elements must be interchanged
· byte temp = new byte[elem_size];
· memcpy(temp, array+jelem_size, elem_size);
· memcpy(array+jelem_size,
· array+(j+1)elem_size,
· elem_size);
· memcpy(array+(j+1)*elem_size, temp, elem_size);
· delete [] temp;
· }
· }
· }
· }
· 注意:由于实现中使用了memcpy,这个这些库函数只能在POD类型中使用
· 在客户端,他们必须实现一个通过地址传递给Bubblesor函数的回调函数,作为一个简单的实现,我写了一个比较两个整形和字符串常量的例子:
· int stdcall CompareInts(const byte velem1, const byte velem2)
· {
· int elem1 = (int)velem1;
· int elem2 = (int)velem2;
·
· if(elem1 < elem2)
· return -1;
· if(elem1 > elem2)
· return 1;
·
· return 0;
· }
·
· int stdcall CompareStrings(const byte velem1, const byte velem2)
· {
· const char elem1 = (char)velem1;
· const char elem2 = (char)velem2;
·
· return strcmp(elem1, elem2);
· }
· 为了将这些进行测试,我编写了一个简单的程序,传递一个包含5个元素的数组以及相应的回调函数指针给Bubblesort()和Quicksort()
· int main(int argc, char argv[])
· {
· int i;
· int array[] = {5432, 4321, 3210, 2109, 1098};
·
· cout << “Before sorting ints with Bubblesort/n”;
· for(i=0; i < 5; i++)
· cout << array[i] << ‘/n’;
·
· Bubblesort((byte)array, 5, sizeof(array[0]), &CompareInts);
·
· cout << “After the sorting/n”;
· for(i=0; i < 5; i++)
· cout << array[i] << ‘/n’;
·
· const char str[5][10] = {“estella”,
· “danielle”,
· “crissy”,
· “bo”,
· “angie”};
·
· cout << “Before sorting strings with Quicksort/n”;
· for(i=0; i < 5; i++)
· cout << str[i] << ‘/n’;
·
· Quicksort((byte*)str, 5, 10, &CompareStrings);
·
· cout << “After the sorting/n”;
· for(i=0; i < 5; i++)
· cout << str[i] << ‘/n’;
·
· return 0;
· }
· 如果我希望以降序排列(最大的元素在第一个),需要做的只是改变回调函数代码或者提供另一个期望的回调函数。
调用约定****
在上面的代码中,你可以看到在函数声明部分有个stdcall。由于它以一个双下划线开始,很显然这是一个编译器特定的扩展,更准确的说是微软特定的扩展。任何支持Win32应用程序开发的编译器都提供这样或者与之等价的扩展。一个通过stdcall标记的函数使用标准的调用约定,之所以这样叫是因为Win32API函数(除了部分带有可变参数列表的外)使用它。遵循标准调用约定的函数,在返回调用者之前先清除堆栈中参数。这是源自Pascal的标准调用约定。但是在C/C++中,调用约定是调用者清理堆栈而不是被调用者。为了强制函数使用C/C++函数调用,__cedecl必须被指定。变参参数函数使用c/c++调用约定。
Windows采用Pascal调用约定是因为它可以减少代码体积,这在早期运行在只有640kb RAM的系统上的Windows是非常重要的。
如果你不喜欢__stdcall,你也可以使用CALLBACK宏,在windef.h中定义如下:
#define CALLBACK __stdcall
or
#define CALLBACK PASCAL
PASCAL被定义为__stdcall
你可以通过连接Calling Convetions in Microsoft Visual C++.获取更多调用约定的信息
C++**成员函数作为回调函数**
由于你可能正在使用C++,希望使用成员函数作为回调函数。但是,如果你进行下面的尝试:
class CCallbackTester
{
public:
int CALLBACK CompareInts(const byte velem1, const byte velem2);
};
Bubblesort((byte*)array, 5, sizeof(array[0]),
&CCallbackTester::CompareInts);
如果使用MS编译器,你将会得到下面的编译错误:
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
这是由于non-static成员函数有一个额外参数,this指针。
这将迫使你使用static成员函数,如果static不能接受,你可以使用某些技术解决这个问题,可以通过下面的链接查找: