Author:Marius Bancila
http://www.codeguru.com/Cpp/Cpp/cpp_mfc/callbacks/article.php/c10557/
翻译by:AllenTing
环境:C/C++/VC++
介绍:
如果你正在读这篇文章,你很可能想知道什么是回调(callback)函数。这篇文章揭示了什么是回调函数,回调函数的好处,为什么你要使用它等等。尽管如此,在学习什么是回调函数前,你必须对函数指针熟悉。如果不是这样,请参考c/c++书籍或者下列链接:
The Syntax of C and C++ Function Pointers
http://www.newty.de/fpt/fpt.html
Pointers to member functions
http://www.parashift.com/c++-faq-lite/pointers-to-members.html
Declaring, Assigning, and Using Function Pointers
http://www.eskimo.com/~scs/cclass/int/sx10a.html
什么是回调函数?
简单的说,回调函数是一个通过函数指针调用的函数。对于一个传递给其它函数的参数,如果你传递的是一个函数的指针(地址),当这个指针被用来调用它所指向的函数时,就被称为构造了一个回调函数。
为什么我们要使用回调函数?
调用者并不关心被调用者是谁;它所知道的是具有特定原型的被调用者以及一些可能的限制(比如,返回值可以是整数,不过特定的值有特定含义)
如果你想知道实践中回调函数多有用,想象一下你想写一个排序算法实现的库(的确很经典),比如冒泡排序,希尔排序,shake排序,快速排序等等。关注点在于为了是你的库更加通用,你不想嵌入排序逻辑(一个数组内两个元素之间哪一个在前)到你的函数中。你想让客户决定哪一种逻辑。或者说,你想让库对各种数据类型都有用(int,floats,strings 等等)。因此,你应该怎样做?你会使用函数指针并且利用回调。
回调被用于通告。举个例子,你需要在你的应用中设置一个计时器。一旦计时器过期,你的应用程序要被告知。然而,计时器机理的实现者并不知道你的应用程序。实际上,只要一个指向给定原型的函数的指针,并且通过使用该指针,构造一个回调来通知你的应用(计时)事件发生既可。的确,SetTimer() WinAPI就是使用回调函数来通知计时器已经过期的。(假如没有提供回调函数,会发送一个消息给应用程序的队列)
另一个使用回调原理并且来自WinAPI函数的例子是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+j*elem_size,
array+(j+1)*elem_size))
{
// the two compared elements must be interchanged
byte* temp = new byte[elem_size];
memcpy(temp, array+j*elem_size, elem_size);
memcpy(array+j*elem_size,
array+(j+1)*elem_size,
elem_size);
memcpy(array+(j+1)*elem_size, temp, elem_size);
delete [] temp;
}
}
}
}
在客户着一边,必须要有一个地址作为Bubblesort()函数参数的回调函数。由于是简单例子,我写了一个比较两个整数和字符串的函数:
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++调用习惯的函数的行为,__cdecl是必须的。可变参数的函数使用c/c++的调用习惯。
windows采用标准调用习惯(即Pascal的习惯)是因为它减少了代码的大小。在早期的windows(运行在640K的内存)这是很重要的。
如果你不喜欢__stdcall , 你可以使用CALLBACK的宏, 它定义在windef.h中,
#define CALLBACK __stdcall
or
#define CALLBACK PASCAL
PASCAL定义为__stdcall
这儿有更多关于调用习惯的参考:
Calling Convetions in Microsoft Visual C++
www.hackcraft.net/cpp/MSCallingConventions/
作为回调函数的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
这个错误是因为非静态成员函数额外的参数,即this指针。
这就强制你使成员函数变为静态的。如果这不可接受,你可以使用好几种技术克服它。用下列链接来学习:
How to Implement Callbacks in C and C++
www.newty.de/fpt/callback.html