原文地址: 原文来源不详
简介
对于很多初学者来说, 往往觉得回调函数很神秘, 很想知道回调函数的工作原理. 本文将要解释什么是回调函数、他们有什么好处、为什么要使用他们等等问题, 在开始之前, 假设你已经熟知了函数指针.
什么是回调函数?
简而言之, 回调函数就是一个通过函数指针调用的函数
. 如果你把函数的指针 (地址) 作为参数传递给另外一个函数, 当这个指针被用为调用它所指向函数时, 我们就说这是回调函数.
为什么要使用回调函数?
因为可以把调用者与被调用者分开. 调用者不关心谁是被调用者, 所有它需知道的, 只是存在一个具有某种特定原型、某些限制条件 (如返回值为 int ) 的被调用函数.
如果想知道回调函数在实际中有什么作用, 先假设有这样一种情况, 我们要编写一个库, 它提供了某些排序算法的实现, 如冒泡排序、快速排序、shell排序、shake排序等等, 但为使库更加通用, 不想在函数中嵌入排序逻辑, 而让使用者来实现相应的逻辑; 或者, 想让库可用于多种数据类型 (int、float、string) , 此时, 该怎么办呢? 可以使用函数指针, 并进行回调.
回调可用于通知机制, 例如, 有时要在程序中设置一个计时器, 每到一定时间, 程序会得到相应的通知, 但通知机制的实现者对我们的程序一无所知. 而此时, 就需要一个特定原型的函数指针, 用这个指针来进行回调, 来通知我们的程序事件已经发生. 实际上, SetTimer() API 使用了一个回调函数来通知计时器, 而且, 万一没有提供回调函数, 它还会把一个消息发往程序的消息队列.
另一个使用回调机制的 API 函数是 EnumWindow(), 它枚举屏幕上所有的顶层窗口, 为每个窗口调用一个程序提供的函数, 并传递窗口的处理程序. 如果被调用者返回一个值, 就继续进行迭代, 否则, 退出. EnumWindow() 并不关心被调用者在何处, 也不关心被调用者用它传递的处理程序做了什么, 它只关系返回值, 因为基于返回值, 它将继续执行或退出.
不管怎么说, 回调函数是继续自 C 语言的, 因而, 在 C++ 中, 应只在与 C 代码建立接口, 或与已有的回调接口打交道, 才使用回调函数. 除了上述情况, 在 C++ 中应用使用虚拟方法或函数符 (functor) , 而不是回调函数.
一个简单的回调函数实现
下面创建了一个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++)
{
//回调比较函数
if (1 == (*cmpFunc)(array + j * elem_size, array + (j + 1) * elem_size)
{
//两个相比较的元素交换
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;
}
}
}
}
注意: 因为实现中使用了 memcpy(), 所以函数在使用的数据类型方面, 会有所局限.
对使用者来说, 必须有一个回调函数, 其地址要传递给 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, 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 << str[i] << '\n';
const char str[5][10] = {"estella", "danielle", "crissy", "bo", "angie"};
cout << "Before sorting string 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 标识的函数使用了, 标准调用约定, 为什么叫标准约定呢, 因为所有的 Win32 API (除了个别接受可变参数的除外) 都使用它. 标准调用约定的函数在它们返回到调用者之前, 都会从堆栈中移除掉参数, 这也是 Pascal 的标准约定. 但在 C/C++ 中, 调用约定是调用负责清理堆栈, 而不是被调用函数; 为强制函数使用 C/C++ 调用约定, 可使用 __cdecl. 另外, 可变参数函数也使用 C/C++ 调用约定.
Windows 操作系统采用了标准调用约定 (Pascal 约定) , 因为其可减小代码的体积. 这点对早期的 Windows 来说非常重要, 因为那时它运行在只有 640KB 内存的电脑上.
如果你不喜欢 __stdcall, 还可以使用 CALLBACK 宏, 它定义在 windef.h 中:
#define CALLBACK __stdcallor
#define CALLBACK PASCALL //而 PASCAL 在此被 #defined 成 __stdcall
作为回调函数的 C++ 方法
因为平时很可能会使用到 C++ 编写代码, 也许会想到把回调函数写成类中的一个方法, 但先来看看一下代码:
class CCallbackTester
{
public:
int CALLBACK CompareInts(const byte* velem1, const byte* velem2);
};
Bubblestort((byte*)array, 5, sizeof(array[0]),&CCallbackTester::CompareInts);
如果使用微软的编译器, 将会得到下面这个编译错误:
error C2664: 'Bubblesort' : cannot convert parameter 4 form '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. 当然, 还有几种方法可以解决这个问题, 但限于篇幅, 就不再论述了.