回调函数

对于很多初学者来说,往往觉得回调函数很神秘,很想知道回调函数的工作原理。本文将要解释什么是回调函数、它们有什么好处、为什么要使用它们等等问题,在开始之前,假设你已经熟知了函数指针。  
  什么是回调函数? 
  简而言之,回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。 
  为什么要使用回调函数? 
  因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为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, 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标识的函数使用了标准调用约定,为什么叫标准约定呢,因为所有的Win32 API(除了个别接受可变参数的除外)都使用它。标准调用约定的函数在它们返回到调用者之前,都会从堆栈中移除掉参数,这也是Pascal的标准约定。但在C/C++中,调用约定是调用者负责清理堆栈,而不是被调用函数;为强制函数使用C/C++调用约定,可使用__cdecl。另外,可变参数函数也使用C/C++调用约定。
  Windows操作系统采用了标准调用约定(Pascal约定),因为其可减小代码的体积。这点对早期的Windows来说非常重要,因为那时它运行在只有640KB内存的电脑上。 
  如果你不喜欢__stdcall,还可以使用CALLBACK宏,它定义在windef.h中: 
#define CALLBACK __stdcallor 
#define CALLBACK PASCAL //而PASCAL在此被#defined成__stdcall  
  作为回调函数的C++方法 
  因为平时很可能会使用到C++编写代码,也许会想到把回调函数写成类中的一个方法,但先来看看以下的代码: 
class CCallbackTester 

 public: 
 int CALLBACK CompareInts(const byte* velem1, const byte* velem2); 
}; 
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。当然,还有几种方法可以解决这个问题,但限于篇幅,就不再论述了。

 

 

 

回调函数是由用户撰写,而由操作系统调用的一类函数,回调函数可以把调用者和被调用者分开,调用者(例如操作系统)不需要关心被调用者到底是哪个函数,它所知道的就是有这么一类函数,这类满足相同的函数签名(函数原型,参数,返回值等),由用户书写完毕后在被调用就可以了。实现上回调函数一般都是通过函数指针来实现的。
典型的回调函数是MFC 下的定时器处理函数ontimer,你只需要添加这个消息响应函数,然后在初始化的时候将ontimer指针传递给操作系统,操作系统就会按照设定好的时间来循环调用ontimer函数了、
你甚至可以将main函数理解成回调函数,因为它不会被客户程序员调用,只会被客户程序员撰写,然后由操作系统来调用。类似的函数 SDK下的窗口过程函数,也是回调函数。
实例代码:
#include <iostream>
using namespace std;
typedef void (*CALLBACK)(int a,int b);
class base
{
private:
int m;
int n;
static CALLBACK func;
public:
void registercallback(CALLBACK fun,int k,int j);
void callcallback();
};
CALLBACK base::func=NULL;
void base::registercallback(CALLBACK fun,int k,int j)
{
func=fun;
m=k;
n=j;
}
void base::callcallback()
{
base::func(m,n);
}
void seiya(int a,int b)
{
cout<<a<<endl<<b<<endl;
cout<<"this is seiya callback function"<<endl;
}
void zilong(int a,int b)
{
cout<<a<<endl<<b<<endl;
cout<<"this is zilong callback function"<<endl;
}
void main(void)
{
  base ba;
  ba.registercallback(seiya,2,3);
  ba.callcallback();
   
  ba.registercallback(zilong,5,6);   
  ba.callcallback();
}

 

 

 

回调函数应该和设计相关而不是和语言相关。在分层设计中,高层次的模块会叫低层次的模块做一些事情,通常是通过函数调用。
从设计上来说,低层次的模块不应该直接调用高层次模块的函数。所以高层次模块在叫低层模块做事的时候会注册一个回调函数给低
层模块,然后低层模块做完了就调用这个函数。表现在C语言上是个函数指针

 

 

 

帮我讲解一下, 谢谢!! 真不懂????
#include<stdio.h>
#include <iostream.h>
typedef int (__stdcall   *PFunc)(int,   int);     //?? 这个不太懂
PFunc func;
  int   __stdcall   Max(int   a,   int   b)   //????也不太懂  __stdcall   
  {   
          return   a>b ?a:b;   
  }   
    
  int  __stdcall Test(PFunc func, int a, int b)   
  {   
          return   func(a, b);   
  }   
    
  void   main()   
  {   
          cout<<Test(Max, 1, 30)<<endl;   
  }

 

 

 

在说回调函数之前,首先要搞清楚什么是函数指针:它就是一个函数在编译时被分配的入口地址,可以将该地址赋给一个指针,这样指针地址变量持有函数入口地址,它就指向了该函数,所以称这种指针为指向函数的指针,简称函数指针。在说明函数指针时,同时也要描述指针所指向的函数的参数类型和个数,如

     int (*funp)(int a , int b) ; 其中funp就是一个函数指针,它指向带有两个int 类型参数的函数。

    在C++中,单独的一个函数名(其后不跟圆括号),被自动的转化为该函数的入口地址,也就是该函数第一条指令的地址。因此,当把一个函数的地址赋给一个指针变量时,对该指针的操作就等同于调用该函数。

    总结一下就是:

    函数指针是一个指向函数的指针变量,它是专门来存放函数入口地址的,在程序中给它赋予哪个函数的入口地址,它就指向哪个函数,因此在一个程序中,一个函数的指针可被多次赋值,指向不同的函数。

    接下来来分析回调函数。

    例子:

     设一个函数process,在调用它的时候,每次实现不同的功能。输入a和b两个数,第一次调用的时找出其中的大者,第二次调用的时找出其中的小者。第三次调用求两者之和。

#include "iostream.h"
int max(int x , int y);
int min(int x , int y);
int add(int x , int y);
void process(int x , int y , int(*fun)(int , int));

//客户程序C
void main()
{
int a,b;
process(a,b,max);//注册回调函数
process(a,b,min);
process(a,b,add);
}

int max(int x , int y)
{
return x>y?x:y;
}

int min(int x , int y)
{
return x<y?x:y;
}

  

int add(int x, int y)
{
return x + y;
}

//服务程序S

void process(int x, int y, int(* fun)(int, int))
{
int result;
result = (*fun)(x , y);
return result;

}

    按照刚才的逻辑,其实所声明的三个功能函数:max ,min ,add 就是回调函数。

     请看:

     使用回调函数实际上就是在调用某个函数时将自己的一个函数(这个函数就是回调函数)的地址作为参数传递给那个函数。而那个函数在需要的时候,利用传递的地址调用回调函数,这是你可以利用这个机会,在回调函数中处理消息或完成一定的操作。

    也可以这样理解:

     所谓回调,就是客户程序C(main)调用服务程序S中的某个函数A(process),然后S又在某个时候反过来调用C中的某个函数B(max),对于C来说,这个B便叫做回调函数。例如Win32下的窗口过程函数就是一个典型的回调函数。


     一般说来,C不会自己调用B,C提供B的目的就是让S来调用它,而且是C不得不提供。由于S并不知道C提供的B叫什么,所以S会约定B的接口规范(函数原型),然后由C提前通过S的一个函数R(process)告诉S,自己将要使用B函数,这个过程称为回调函数的注册,R称为注册函数。

      下面举个通俗的例子:
      某天,我打电话向你请教问题,当然是个难题,:),你一时想不出解决方法,我又不能拿着电话在那里傻等,于是我们约定:等你想出办法后打手机通知我,这样,我就挂掉电话办其它事情去了。过了XX分钟,我的手机响了,你兴高采烈的说问题已经搞定,应该如此这般处理。故事到此结束。
这个例子说明了“异步+回调”的编程模式。其中,你后来打手机告诉我结果便是一个“回调”过程;我的手机号码必须在以前告诉你,这便是注册回调函数;我的手机号码应该有效并且手机能够接收到你的呼叫,这是回调函数必须符合接口规范。

        

         2. 什么情况下使用回调


      如果你是SDK的使用者,一旦别人制定了回调机制,那么你被迫得使用回调函数,因此这个问题只对SDK设计者有意义。
从引入的目的看,回调大致分为三种:
1) SDK有消息需要通知应用程序,比如定时器被触发;
2) SDK的执行需要应用程序的参与,比如SDK需要你提供一种排序算法;
3) SDK的操作比较费时,但又不能让应用程序阻塞在那里,于是采用异步方式,让调用函数及时返回,SDK另起线程在后台执行操作,待操作完成后再将结果通知应用程序。
经上面这样一总结,你也许会恍然大悟:原来“回调机制”无处不在啊!
是的,不光是Win32 API编程中你会用到,也不光是其它SDK编程中会用到,平时我们自己编写程序时也可能用到回调机制,这时,我们既是回调的设计者又是回调的使用者。

        3. 传统SDK回调函数设计模式


     Win32 SDK是这方面的典型例子,这类SDK的函数接口都是基于C语言的,SDK或者提供专门的注册函数,用于注册回调函数的地址,或者是在调用某个方法时才传入回调函数的地址,回调函数的原型也由于注册函数中的函数指针定义而受到约束。
以Win32中的多媒体定时器函数为例,其原型为:


MMRESULT timeSetEvent(
UINT uDelay, // 定时器时间间隔,以毫秒为单位              
UINT uResolution,           
LPTIMECALLBACK lpTimeProc, // 回调函数地址
DWord dwUser, // 用户设定的数据            

UINT fuEvent                
);

其中第三个参数便是用于注册回调函数的,第四个参数用于设定用户自定义数据,

        下面是回调函数的具体使用方法:
#include "stdio.h"
#include "windows.h"


#include "mmsystem.h" // 多媒体定时器需要包含此文件
#pragma comment(lib, "winmm.lib") // 多媒体定时器需要导入此库

void CALLBACK timer_proc(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2) // 定义回调函数
{
printf("time out.\n");
}

int main()
{
UINT nId = timeSetEvent( 1000, 0, timer_proc, 0, TIME_CALLBACK_FUNCTIONTIME_PERIODIC); // 注册回调函数
getchar();
timeKillEvent( nId );
return 0;
}


     运行程序,我们会看到,屏幕上每隔一秒将会打印出“time out.”信息。同时我们也应该注意到,这里的timeSetEvent是异步执行的,timeSetEvent很快执行完毕,主线程继续执行,操作系统在后台负责检查timer是否超时。

前面已经说过,本文的是站在SDK设计者的角度来看待问题,这里,我们就把前面那个通俗的例子变成程序,如下:
/// sdk.h (SDK头文件)
#ifndef __SDK_H__
#define __SDK_H__

typedef void (*HELP_CALLBACK)(const char*); // 回调函数指针
void help_me( const char* question, HELP_CALLBACK callback ); // 接口声明

#endif//__SDK_H__

/// sdk.cpp (SDK源文件,为方便,没有使用.c文件)


#include "sdk.h"
#include "stdio.h"
#include "windows.h"

HELP_CALLBACK g_callback;

void do_it() // 处理函数
{
printf("thinking...\n");
Sleep( 3000 );
printf("think out.\n");
printf("call him.\n");
g_callback( "2." );
}

void help_me( const char* question, HELP_CALLBACK callback ) // 接口实现
{
g_callback = callback; // 保存回调函数指针
printf("help_me: %s\n", question);
do_it(); // 如果采用异步方式的话,这里一般采用创建线程的方式
}

/// app.cpp (应用程序源文件)
#include "sdk.h"

#include "stdio.h"


void got_answer( const char* msg ) // 定义回调函数
{
printf("got_answer: %s\n", msg);
}
int main()
{
help_me( "1+1=?", got_answer ); // 使用SDK,注册回调函数
return 0;
}


4. C++中回调函数的设计


     C++的类中也可以使用类似上面的设计方式。如果SDK采用C语言接口,应用程序使用C++编程方式,那么类成员函数由于具有隐含的this指针而不能赋值给普通函数指针,解决方法很简单,就是为其加上static关键字。


以上面的程序为例,这里我们只看应用程序的代码:
/// app.cpp ( C++风格 )
#include "sdk.h"
#include "stdio.h"

class App
{
public:


void ask( const char* question )
{
help_me( question, got_answer );
}


static void got_answer( const char* msg )
{
printf("got_answer: %s\n", msg);
}


};

int main()
{
App app;
app.ask( "1+1=?");
return 0;
}


    上面这种方式有个明显的缺点:由于got_answer是静态成员函数,所以它不能访问类的非静态成员变量,这可不是一件好事情。为了解决此问题,作为回调函数的设计者,你有必要为其增添一个参数,用于传递用户所需的值,如下:


/// sdk.h (SDK头文件)
#ifndef __SDK_H__
#define __SDK_H__

typedef void (*HELP_CALLBACK)(const char*, unsigned long); // 回调函数指针
void help_me( const char* question, HELP_CALLBACK callback, unsigned long user_value ); // 接口声明

#endif//__SDK_H__

/// sdk.cpp (SDK源文件,为方便,没有使用.c文件)


#include "sdk.h"
#include "stdio.h"
#include "windows.h"

HELP_CALLBACK g_callback;
unsigned long g_user_value;

void do_it()
{
printf("thinking...\n");
Sleep( 3000 );
printf("think out.\n");
printf("call him.\n");
g_callback( "2.", g_user_value ); // 将用户设定的数据传入
}

void help_me( const char* question, HELP_CALLBACK callback, unsigned long user_value )
{
g_callback = callback;
g_user_value = user_value; // 保存用户设定的数据
printf("help_me: %s\n", question);
do_it();
}

/// app.cpp (应用程序源文件)
#include "sdk.h"
#include "stdio.h"

#include "assert.h"


class App
{
public:
App( const char* name ) : m_name(name)

{
}
void ask( const char* question )
{
help_me( question, got_answer, (unsigned long)this ); // 将this指针传入
}
static void got_answer( const char* msg, unsigned long user_value )
{
App* pthis = (App*)user_value; // 转换成this指针,以访问非静态数据成员
assert( pthis );
printf("%s got_answer: %s\n", pthis->m_name, msg);
}
protected:
const char* m_name;
};

int main()
{
App app("ABC");
app.ask( "1+1=?");
return 0;
}


     这里的user_value被设计成unsigned long,它既可以传递整型数据,也可以传递一个地址值(因为它们在32位机器上宽度都为32),有了地址,那么像结构体变量、类对象等都可以访问了。

5. C++中回调类编程模式


     时代在不断进步,SDK不再是古老的API接口,C++面向对象编程被广泛的用到各种库中,因此回调机制也可以采用C++的一些特性来实现。
通过前面的讲解,其实我们不难发现回调的本质便是:SDK定义出一套接口规范,应用程序按照规定实现它。这样一说是不是很简单,

想想我们C++中的继承,想想我们亲爱的抽象基类......于是,我们得到以下的代码:


/// sdk.h
#ifndef __SDK_H__
#define __SDK_H__

class Notifier // 回调类,应用程序需从此派生
{
public:
virtual ~Notifier() { }
virtual void got_answer(const char* answer) = 0; // 纯虚函数,用户必须实现它
};

class Sdk // Sdk提供服务的类
{
public:
Sdk(Notifier* pnotifier); // 用户必须注册指向回调类的指针
void help_me(const char* question);
protected:
void do_it(); 
protected:
Notifier* m_pnotifier; // 用于保存回调类的指针
};

#define//__SDK_H__

/// sdk.cpp
#include "sdk.h"
#include "windows.h"
#include <iostream>
using namespace std;

Sdk::Sdk(Notifier* pnotifier) : m_pnotifier(pnotifier)
{
}

void Sdk::help_me(const char* question)
{
cout << "help_me: " << question << endl;


do_it(); 
}


void Sdk::do_it()
{
cout << "thinking..." << endl;
Sleep( 3000 );
cout << "think out." << endl;
cout << "call him." << endl;
m_pnotifier->got_answer( "2." );

}

/// app.cpp
#include "sdk.h"

class App : public Notifier // 应用程序实现一个从回调类派生的类
{
public:
App( const char* name ) : m_sdk(this), m_name(name) // 将this指针传入
{
}
void ask( const char* question )
{
m_sdk.help_me( question );
}
void got_answer( const char* answer ) // 实现纯虚接口
{
cout << m_name << " got_answer: " << answer << endl;
}
protected:
Sdk m_sdk;
const char* m_name;
};

int main()
{
App app("ABC");
app.ask( "1+1=?");
return 0;
}


        Notifier将用户必须实现的回调函数都以纯虚函数的方式定义,这样用户就不得不实现它,当然,不是非实现不可,那么我们也可以将其定义成一般的虚函数,并像析构函数那样提供一个“空”的实现,这样用户可以在关心它时才去实现它。由于这个类的作用是实现回调,所以我们不妨称之为回调类。
上面这种方式一简化,可以将Sdk与Notifier合并在一起。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值