C/C++回调函数--B作者

原文地址: 原文来源不详

简介

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

什么是回调函数?

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

为什么要使用回调函数?

因为可以把调用者与被调用者分开. 调用者不关心谁是被调用者, 所有它需知道的, 只是存在一个具有某种特定原型、某些限制条件 (如返回值为 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. 当然, 还有几种方法可以解决这个问题, 但限于篇幅, 就不再论述了.

你可能感兴趣的:(C/C++回调函数--B作者)