C常用区别

 

最主要的用途就是当函数不处在同一个文件当中,比如动态库,要调用其他程序中的函数就只有采用回调的形式;

 

回调函数是一个程序员不能显式调用的函数;通过将回调函数的地址传给调用者从而实现调用。回调函数使用是必要的,在我们想通过一个统一接口实现不 同的内容,这时用回掉函数非常合适。比如,我们为几个不同的设备分别写了不同的显示函数:void TVshow();void ComputerShow(); void NoteBookShow()...等 等。这是我们想用一个统一的显示函数,我们这时就可以用回掉函数了。void show(void (*ptr)()); 使用时根据所传入的参数不同而调用不同的回调函数。

回调函数是由用户撰写,而由操作系统调用的一类函数,回调函数可以把调用者和被调用者分开,调用者(例如操作系统)不需要关心被调用者到底是哪个函数,它所知道的就是有这么一类函数,这类满足相同的函数签名(函数原型,参数,返回值等),由用户书写完毕后在被调用就可以了。实现上回调函数一般都是通过函数指针来实现的。
    典型的回调函数是MFC 下的定时器处理函数ontimer,你只需要添加这个消息响应函数,然后在初始化的时候将ontimer指针传递给操作系统,操作系统就会按照设定好的时间来循环调用ontimer函数了;
    你甚至可以将main函数理解成回调函数,因为它不会被客户程序员调用,只会被客户程序员撰写,然后由操作系统来调用。类似的函数 SDK下的窗口过程函数,也是回调函数。


精妙比喻:
回调函数还真有点像您随身带的BP机:告诉别人号码,在它有事情时Call您。

回调用于层间协作,上层将本层函数安装在下层,这个函数就是回调,而下层在一定条件下触发回调,例如作为一个驱动,是一个底层,他在收到一个数据时,除了完成本层的处理工作外,还将进行回调,将这个数据交给上层应用层来做进一步处理,这在分层的数据通信中很普遍。其实回调和API非常接近,他们的共性都是跨层调用的函数。但区别是API是低层提供给高层的调用,一般这个函数对高层都是已知的;而回调正好相反,他是高层提供给底层的调用,对于低层他是未知的,必须由高层进行安装,这个安装函数其实就是一个低层提供的API,安装后低层不知道这个回调的名字,但它通过一个函数指针来保存这个回调,在需要调用时,只需引用这个函数指针和相关的参数指针。 其实:回调就是该函数写在高层,低层通过一个函数指针保存这个函数,在某个事件的触发下,低层通过该函数指针调用高层那个函数。

 

到目前为止,我们只讨论了函数指针及回调而没有去注意ANSI C/C++的编译器规范。许多编译器有几种调用规范。如在Visual C++中,可以在函数类型前加_cdecl,_stdcall或者_pascal来表示其调用规范(默认为_cdecl)。C++ Builder也支持_fastcall调用规范。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。
将调用规范看成是函数类型的一部分是很重要的;不能用不兼容的调用规范将地址赋值给函数指针。例如:

// 被调用函数是以int为参数,以int为返回值
__stdcall int callee(int);

// 调用函数以函数指针为参数
void caller( __cdecl int(*ptr)(int));

// 在p中企图存储被调用函数地址的非法操作
__cdecl int(*p)(int) = callee; // 出错

指针p和callee()的类型不兼容,因为它们有不同的调用规范。因此不能将被调用者的地址赋值给指针p,尽管两者有相同的返回值和参数列

 

回调函数问题

 void DLLDIR__stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunctioncmpFunc);
  void DLLDIR__stdcall Quicksort(byte* array,int size,int elem_size,CompareFunctioncmpFunc);
 

 这两个函数接受以下参数:

  ·byte * array:指向元素数组指针(任意类型)。

  ·int size:数组中元素的个数。

  ·int elem_size:数组中一个元素的大小,以字节为单位。

·CompareFunction cmpFunc:带有上述原型的指向回调函数的指针。

基于以上约定,函数Bubblesort()的实现如下,Quicksort()就稍微复杂一点:

  void DLLDIR __stdcall Bubblesort(byte* array,int size,intelem_size,CompareFunction cmpFunc)
  {
           for(int i=0; i < size; i++)
           {
                     for(int j=0; j < size-i-1; j++)
                     {
                     <span style="white-space:pre">		</span>//回调比较函数
  <span style="white-space:pre">				</span>if(1 ==(*cmpFunc)(array+j*elem_size,array+(j+1)*elem_size))
  <span style="white-space:pre">				</span>{
  <span style="white-space:pre">					</span>//两个相比较的元素相交换
  <span style="white-space:pre">					</span>byte* temp = newbyte[elem_size];
  <span style="white-space:pre">	</span><span style="white-space:pre">				</span>memcpy(temp,array+j*elem_size,elem_size);
 <span style="white-space:pre">					</span>memcpy(array+j*elem_size,array+(j+1)*elem_size,elem_size);
  <span style="white-space:pre">					</span>memcpy(array+(j+1)*elem_size,temp,elem_size);
  <span style="white-space:pre">					</span>delete [] temp;
 <span style="white-space:pre">				</span> }
  <span style="white-space:pre">			</span>}
  <span style="white-space:pre">		</span>}
  }

  注意:因为实现中使用了memcpy(),所以函数在使用的数据类型方面,会有所局限。

  对使用者来说,必须有一个回调函数,其地址要传递给Bubblesort()函数。下面有二个简单的示例,一个比较两个整数,而另一个比较两个字符串:

  int __stdcall CompareInts(const byte* velem1,const byte* velem2)
  {
  <span style="white-space:pre">	</span>int elem1 =*(int*)velem1;
  <span style="white-space:pre">	</span>int elem2 =*(int*)velem2;
  <span style="white-space:pre">	</span>if(elem1 < elem2)
  <span style="white-space:pre">		</span>return -1;
  <span style="white-space:pre">	</span>if(elem1 > elem2)
  <span style="white-space:pre">		</span>return 1;
  <span style="white-space:pre">	</span>return 0;
  }
  int __stdcall CompareStrings(const byte* velem1,const byte* velem2)
  {
  <span style="white-space:pre">	</span>const char* elem1 =(char*)velem1;
  <span style="white-space:pre">	</span>const char* elem2 =(char*)velem2;
  <span style="white-space:pre">	</span>return strcmp(elem1,elem2);
  }

  下面另有一个程序,用于测试以上所有的代码,它传递了一个有5个元素的数组给Bubblesort()和Quicksort(),同时还传递了一个指向回调函数的指针。(使用byte类型需包含头文件windows.h,或typedefunsigned char byte)

  int main[1](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++)
  <span style="white-space:pre">	</span>cout << array[i]<< '\n';
  Bubblesort((byte*)array,5,sizeof(array[0]),&CompareInts);
  cout <<"After the sorting\n";
  for(i=0; i < 5;i++)
  <span style="white-space:pre">	</span>cout << array[i]<< '\n';
  const charstr[5][10] ={"estella","danielle","crissy","bo","angie"};
  cout <<"Before sorting strings with Quicksort\n";
  for(i=0; i < 5;i++)
 <span style="white-space:pre">	</span>cout << str[i]<< '\n';
  Quicksort((byte*)str,5,10,&CompareStrings);
  cout <<"After the sorting\n";
  for(i=0; i < 5;i++)
  <span style="white-space:pre">	</span>cout << str[i]<< '\n';
  return 0;
  }
 

 

函数原型中找到__stdcall,因为它以双下划线打头,所以它是一个特定于编译器的扩展,说到底也就是微软的实现。任何支持开发基于Win32的程序都必须支持这个扩展或其等价物。以__stdcall标识的函数使用了标准调用约定,为什么叫标准约定呢,因为所有的Win32 API(除了个别接受可变参数的除外)都使用它。

标准调用约定的函数在它们返回到调用者之前,都会从堆栈中移除掉参数,这也是Pascal的标准约定。但在C/C++中,调用约定是调用者负责清理堆栈,而不是被调用函数;为强制函数使用C/C++调用约定,可使用__cdecl。另外,可变参数函数也使用C/C++调用约定。

  

1 什么是回调

软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;异步调用是一种类似消息或事件的机制, 不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。同步调用是三者当中最简单的,而回调又常常是异步调用的基础,因此,下面我们着重讨论回 调机制在不同软件架构中的实现。


对于不同类型的语言(如结构化语言和对象语言)、平台(Win32、JDK)或构架(CORBA、DCOM、WebService),客户和服务的 交互除了同步方式以外,都需要具备一定的异步通知机制,让服务方(或接口提供方)在某些情况下能够主动通知客户,而回调是实现异步的一个最简捷的途径。

对于一般的结构化语言,可以通过回调函数来实现回调。回调函数也是一个函数或过程,不过它是一个由调用方自己实现,供被调用方使用的特殊函数。

在面向对象的语言中,回调则是通过接口或抽象类来实现的,我们把实现这种接口的类成为回调类,回调类的对象成为回调对象。对于象C++或Object Pascal这些兼容了过程特性的对象语言,不仅提供了回调对象、回调方法等特性,也能兼容过程语言的回调函数机制。

Windows平台的消息机制也可以看作是回调的一种应用,我们通过系统提供的接口注册消息处理函数(即回调函数),从而实现接收、处理消息的目的。由于Windows平台的API是用C语言来构建的,我们可以认为它也是回调函数的一个特例。

对于分布式组件代理体系CORBA,异步处理有多种方式,如回调、事件服务、通知服务等。事件服务和通知服务是CORBA用来处理异步消息的标准服务,他们主要负责消息的处理、派发、维护等工作。对一些简单的异步处理过程,我们可以通过回调机制来实现。

下面我们集中比较具有代表性的语言(C、Object Pascal)和架构(CORBA)来分析回调的实现方式、具体作用等。

2 过程语言中的回调(C)

2.1 函数指针

回调在C语言中是通过函数指针来实现的,通过将回调函数的地址传给被调函数从而实现回调。因此,要实现回调,必须首先定义函数指针,请看下面的例子:

void Func(char *s);// 函数原型

void (*pFunc) (char *);//函数指针

 

可以看出,函数的定义和函数指针的定义非常类似。

一般的化,为了简化函数指针类型的变量定义,提高程序的可读性,我们需要把函数指针类型自定义一下。

typedef void(*pcb)(char *);

 

回调函数可以象普通函数一样被程序调用,但是只有它被当作参数传递给被调函数时才能称作回调函数。

被调函数的例子:

void GetCallBack(pcb callback)

{

/*do something*/

}

用户在调用上面的函数时,需要自己实现一个pcb类型的回调函数:

void fCallback(char *s)

{

/* do something */

}

然后,就可以直接把fCallback当作一个变量传递给GetCallBack,

GetCallBack(fCallback);

 

如果赋了不同的值给该参数,那么调用者将调用不同地址的函数。赋值可以发生在运行时,这样使你能实现动态绑定。

 

2.2 参数传递规则

到目前为止,我们只讨论了函数指针及回调而没有去注意ANSI C/C++的编译器规范。许多编译器有几种调用规范。如在Visual C++中,可以在函数类型前加_cdecl,_stdcall或者_pascal来表示其调用规范(默认为_cdecl)。C++ Builder也支持_fastcall调用规范。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。

将调用规范看成是函数类型的一部分是很重要的;不能用不兼容的调用规范将地址赋值给函数指针。例如:

// 被调用函数是以int为参数,以int为返回值

__stdcall int callee(int);

 

// 调用函数以函数指针为参数

void caller( __cdecl int(*ptr)(int));

 

// 在p中企图存储被调用函数地址的非法操作

__cdecl int(*p)(int) = callee; // 出错

 

指针p和callee()的类型不兼容,因为它们有不同的调用规范。因此不能将被调用者的地址赋值给指针p,尽管两者有相同的返回值和参数列

2.3 应用举例

C语言的标准库函数中很多地方就采用了回调函数来让用户定制处理过程。如常用的快速排序函数、二分搜索函数等。

快速排序函数原型:

void qsort(void *base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void *, const void *));

二分搜索函数原型:

void *bsearch(const void *key, const void *base, size_t nelem,

                                size_t width, int (_USERENTRY *fcmp)(const void *, const void *));

 

其中fcmp就是一个回调函数的变量。

下面给出一个具体的例子:

#include <stdio.h>

#include <stdlib.h>

 

int sort_function( const void *a, const void *b);

int list[5] = { 54, 21, 11, 67, 22 };

 

int main(void)

{

   int  x;

 

   qsort((void *)list, 5, sizeof(list[0]), sort_function);

   for (x = 0; x < 5; x++)

      printf("%i\n", list[x]);

   return 0;

}

int sort_function( const void *a, const void *b)

{

   return *(int*)a-*(int*)b;

}

 

2.4 面向对象语言中的回调(Delphi)

Dephi与C++一样,为了保持与过程语言Pascal的兼容性,它在引入面向对象机制的同时,保留了以前的结构化特性。因此,对回调的实现,也有两种截然不同的模式,一种是结构化的函数回调模式,一种是面向对象的接口模式。

2.4.1 回调函数

回调函数类型定义:

type

   TCalcFunc=function (a:integer;b:integer):integer;

按照回调函数的格式自定义函数的实现,如

function Add(a:integer;b:integer):integer

begin

  result:=a+b;

end;

function Sub(a:integer;b:integer):integer

begin

  result:=a-b;

end;

回调的使用

function Calc(calc:TcalcFunc;a:integer;b:integer):integer

下面,我们就可以在我们的程序里按照需要调用这两个函数了

c:=calc(add,a,b);//c=a+b

c:=calc(sub,a,b);//c=a-b

2.4.2 回调对象

什么叫回调对象呢,它具体用在哪些场合?首先,让我们把它与回调函数对比一下,回调函数是一个定义了函数的原型,函数体则交由第三方来实现的一种动态应用模式。要实现一个回调函数,我们必须明确知道几点:该函数需要那些参数,返回什么类型的值。同样,一个回调对象也是一个定义了对象接口,但是没有具 体实现的抽象类(即接口)。要实现一个回调对象,我们必须知道:它需要实现哪些方法,每个方法中有哪些参数,该方法需要放回什么值。

因此,在回调对象这种应用模式中,我们会用到接口。接口可以理解成一个定义好了但是没有实现的类,它只能通过继承的方式被别的类实现。Delphi 中的接口和COM接口类似,所有的接口都继承与IInterface(等同于IUnknow),并且要实现三个基本的方法QueryInterface, _AddRef, 和_Release。

  • 定义一个接口

type IShape=interface(IInterface)

        procedure Draw;

end

  • 实现回调类

type TRect=class(TObject,IShape)

        protected

      function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

      function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

    public

          procedure Draw;

end;

 

type TRound=class(TObject,IShape)

        protected

      function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

      function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

    public

          procedure Draw;

end;

  • 使用回调对象

procedure MyDraw(shape:IShape);

var

shape:IShape;

begin

shape.Draw;

end;

如果传入的对象为TRect,那么画矩形;如果为TRound,那么就为圆形。用户也可以按照自己的意图来实现IShape接口,画出自己的图形:

MyDraw(Trect.Create);

MyDraw(Tround.Create);

2.4.3 回调方法

回调方法(Callback Method)可以看作是回调对象的一部分,Delphi对windows消息的封装就采用了回调方法这个概念。在有些场合,我们不需要按照给定的要求实现整个对象,而只要实现其中的一个方法就可以了,这是我们就会用到回调方法。

回调方法的定义如下:

TNotifyEvent = procedure(Sender: TObject) of object;

TMyEvent=procedure(Sender:Tobject;EventId:Integer) of object;

TNotifyEvent 是Delphi中最常用的回调方法,窗体、控件的很多事件,如单击事件、关闭事件等都是采用了TnotifyEvent。回调方法的变量一般通过事件属性的方式来定义,如TCustomForm的创建事件的定义:

property OnCreate: TNotifyEvent read FOnCreate write FOnCreate stored IsForm;

我们通过给事件属性变量赋值就可以定制事件处理器。

用户定义对象(包含回调方法的对象):

type TCallback=Class

    procedure ClickFunc(sender:TObject);

end;

procedure Tcallback.ClickFunc(sender:TObject);

begin

  showmessage('the caller is clicked!');

end;

窗体对象:

type TCustomFrm=class(TForm)

  public

        procedure RegisterClickFunc(cb:procedure(sender:Tobject) of object);

end;

 

procedure TcustomFrm..RegisterClickFunc(cb:TNotifyEvent);

begin

  self.OnClick=cb;

end;

使用方法:

var

  frm:TcustomFrm;

begin

  frm:=TcustomFrm.Create(Application);

  frm.RegisterClickFunc(Tcallback.Create().ClickFunc);

end;

 

3 回调在分布式计算中的应用(CORBA)

3.1 回调接口模型

CORBA的消息传递机制有很多种,比如回调接口、事件服务和通知服务等。回调接口的原理很简单,CORBA客户和服务器都具有双重角色,即充当服务器也是客户客户。

回调接口的反向调用与正向调用往往是同时进行的,如果服务端多次调用该回调接口,那么这个回调接口就变成异步接口了。因此,回调接口在CORBA中 常常充当事件注册的用途,客户端调用该注册函数时,客户函数就是回调函数,在此后的调用中,由于不需要客户端的主动参与,该函数就是实现了一种异步机制。

从CORBA规范我们知道,一个CORBA接口在服务端和客户端有不同的表现形式,在客户端一般使用桩(Stub)文件,服务端则用到框架 (Skeleton)文件,接口的规格采用IDL来定义。而回调函数的引入,使得服务端和客户端都需要实现一定的桩和框架。下面是回调接口的实现模型:


3.1.1 范例


下面给出了一个使用回调的接口文件,服务端需要实现Server接口的框架,客户端需要实现CallBack的框架:

module cb

{

        interface CallBack;

        interface Server;

 

interface CallBack

{

        void OnEvent(in long Source,in long msg);

};

        interface Server

{

        long RegisterCB(in CallBack cb);

               void UnRegisterCB(in long hCb);

};

};

 

客户端首先通过同步方式调用服务端的接口RegistCB,用来注册回调接口CallBack。服务端收到该请求以后,就会保留该接口引用,如果发生某种事件需要向客户端通知的时候就通过该引用调用客户方的OnEvent函数,以便对方及时处理。

 

 不同的编程语言可能有不同的语法,下面举一个c语言中回调函数的例子, 其中一个回调函数不带参数,另一个回调函数带参数。
      例子1:

//Test.c 
#include <stdlib.h> 
#include <stdio.h> 

int Test1() 
{ 
  int i; 
  for (i=0; i<30; i++) 
  { 
    printf("The %d th charactor is: %c\n", i,(char)('a' + i%26)); 
  } 
  return 0; 
} 

int Test2(int num) 
{ 
  int i; 
  for (i=0; i<num; i++) 
  { 
   printf("The %d th charactor is: %c\n", i,(char)('a' + i%26)); 
  } 
  return 0; 
} 

void Caller1( void (*ptr)() )  //指向函数的指针作函数参数 
{ 
  (*ptr)(); 
}
 
void Caller2(int n,  int (*ptr)() )
//指向函数的指针作函数参数,这里第一个参数是为指向函数的指针服务的, 
{                       //不能写成voidCaller2(int (*ptr)(int n)),这样的定义语法错误。 
  (*ptr)(n); 
  return; 
}
 
int main() 
{ 
  printf("************************\n"); 
  Caller1(Test1); //相当于调用Test2(); 
  printf("&&&&&&************************\n");
  Caller2(30, Test2); //相当于调用Test2(30); 
  return 0; 
} 


      以上通过将回调函数的地址传给调用者从而实现调用,但是需要注意的是带 参回调函数的用法。要实现回调,必须首先定义函数指针。函数指针的定义这里稍微提一下。比如:
    int (*ptr)(); 这里ptr是一个函数指针,其中(*ptr)的括号不能省略,因为括号的优先级高于星号,那样就成了一个返回类型为整型指针的函数声明了

#include "stdio.h"
#include "conio.h"
int add(int a, int b);
int libfun(int (*pDis)(int a, int b));

int main(void)
{
         int (*pfun)(int a, int b);
         pfun = add;
         libfun(pfun);
}

int add(int a, int b)
{
         return a + b;
}

int libfun(int (*pDis)(int a, int b))
{
         int a, b;
         a = 1;
         b = 2;
         printf("%d", pDis(a, b));
}

现在这几个函数是在同一个文件当中;
假如 :
int libfun(int (*pDis)(int a, int b))
是一个库中的函数,就只有使用回调了,通过函数指针参数将外部函数地址传入来实现调用;

函数 add 的代码作了修改,也不必改动库的代码,就可以正常实现调用;便于程序的维护和升级;

 

2 以上的概念叙述很难让你明白回调函数到底是怎么一回事儿下面通过一个实例描述一下,请耐心看下去,此示例由三个文件组成:

para_callback.h、para_callback.c、callback.c三者的代码如下:

 

 /**************************para_callback.h**************************/
#ifndefPARA_CALLBACK_H
#defPARA_CALLBACK_H
//声明一个函数指针类型callback_t,用callback_t声明的变量指向这样的函
//数无返回值有一个参数参数类型为空指针类型
typedefvoid (*callback_t)(void *);
voidspeak(callback_t, void*);
 #endif
 
/**************************para_callback.c**************************/
#include"para_callback.h" 
void speak(callback_t func, void* variable)
//这就是下面论述中的“实现者”
{
     func(variable);
}
 
 
/****************************callback.c****************************/#include"para_callback.h"
#include<stdio.h>
 
voidspeakint(void* variable)//这就是传说中的回调函数中的一个
{
     printf("%s%d","I am ainteger variable: ",*((int *)variable));
}
 
voidspeakfloat(void* variable)//这就是传说中的回调函数中的一个
{
     printf("%s%f","I am a floatvariable: ",*((float *)variable));
}
 
 int main()//在本例中main函数就是下面论述中的“调用者”
{    
int variable_int = 100;    
float variable_float = 100.9;    
speak(speakint,(void*)&variable_int);    
speak(speakfloat,(void*)&variable_float);
}
 


在本例中回调函数的参数按什么类型解释由调用者规定,对于实现者来说就是一个void*指针,实现者只负责将这个指针转交给回调函数,而不关心它到底指向什么数据类型。

调用者知道自己传的参数是int*float*类型的,那么在自己的回调函数中就应该知道参数要转换成int*型和float*型来解释。

 

概括起来,回调机制包括两部分:服务执行者和服务方式制定者。
1. 服务执行者先制定服务规范;
2.服务方式制定者然后按照规范制定服务方式;
3.然后执行者按照这个方式提供服务。
    回调函数的方式是把函数指针的作为参数传递进去,所以所谓的规范就是约定函数的参数类型、个数。

调用(calling)机制从汇编时代起已经大量使用:准备一段现成的代码,调用者可以随时跳转至此段代码的起始地址,执行完后再返回跳转时的后续地址。 CPU为此准备了现成的调用指令,调用时可以压栈保护现场,调用结束后从堆栈中弹出现场地址,以便自动返回。借堆栈保护现场真是一项绝妙的发明,它使调用者和被调者可以互不相识,于是才有了后来的函数和构件,使吾辈编程者如此轻松愉快。若评选对人类影响最大之发明,在火与车轮之后,笔者当推压栈调用。
    话虽这样说,此调用机制并非完美。回调函数就是一例。函数之类本是为调用者准备的美餐,其烹制者应对食客了如指掌,但实情并非如此。例如,写一个快速排序函数供他人调用,其中必包含比较大小。麻烦来了:此时并不知要比较的是何类数据--整数、浮点数、字符串?于是只好为每类数据制作一个不同的排序函数。更通行的办法是在函数参数中列一个回调函数地址,并通知调用者:君需自己准备一个比较函数,其中包含两个指针类参数,函数要比较此二指针所指数据之大小,并由函数返回值说明比较结果。排序函数借此调用者提供的函数来比较大小,借指针传递参数,可以全然不管所比较的数据类型。被调用者回头调用调用者的函数(够咬嘴的),故称其为回调(callback
    回调函数使程序结构乱了许多。Windows API 函数集中有不少回调函数,尽管有详尽说明,仍使初学者一头雾水。恐怕这也是无奈之举。无论何种事物,能以树形结构单向描述毕竟让人舒服些。如果某家族中孙辈又是某祖辈的祖辈,恐怕无人能理清其中的头绪。但数据处理之复杂往往需要构成网状结构,非简单的客户/服务器关系能穷尽。
    Windows 系统还包含着另一种更为广泛的回调机制,即消息机制。消息本是 Windows 的基本控制手段,乍看与函数调用无关,其实是一种变相的函数调用。发送消息的目的是通知收方运行一段预先准备好的代码,相当于调用一个函数。消息所附带的 WParam 和 LParam 相当于函数的参数,只不过比普通参数更通用一些。应用程序可以主动发送消息,更多情况下是坐等 Windows 发送消息。一旦消息进入所属消息队列,便检感兴趣的那些,跳转去执行相应的消息处理代码。

操作系统本是为应用程序服务,由应用程序来调用。而应用程序一旦启动,却要反过来等待操作系统的调用。这分明也是一种回调,或者说是一种广义回调。其实,应用程序之间也可以形成这种回调。假如进程 B 收到进程 A 发来的消息,启动了一段代码,其中又向进程 A 发送消息,这就形成了回调。这种回调比较隐蔽,弄不好会搞成递归调用,若缺少终止条件,将会循环不已,直至把程序搞垮。若是故意编写成此递归调用,并设好终止条件,倒是很有意思。但这种程序结构太隐蔽,除非十分必要,还是不用为好。
    利用消息也可以构成狭义回调。上面所举排序函数一例,可以把回调函数地址换成窗口handle。如此,当需要比较数据大小时,不是去调用回调函数,而是借 API 函数 SendMessage 向 指定窗口发送消息。收到消息方负责比较数据大小,把比较结果通过消息本身的返回值传给消息发送方。所实现的功能与回调函数并无不同。当然,此例中改为消息纯属画蛇添脚,反倒把程序搞得很慢。但其他情况下并非总是如此,特别是需要异步调用时,发送消息是一种不错的选择。假如回调函数中包含文件处理之类的低 速处理,调用方等不得,需要把同步调用改为异步调用,去启动一个单独的线程,然后马上执行后续代码,其余的事让线程慢慢去做。一个替代办法是借 API 函数 PostMessage发送一个异步消息,然后立即执行后续代码。这要比自己搞个线程省事许多,而且更安全。
    如今我们是活在一个object时代。只要与编程有关,无论何事都离不开 object。但object 并未消除回调,反而把它发扬光大,弄得到处都是,只不过大都以事件(event)的身份出现,镶嵌在某个结构之中,显得更正统,更容易被人接受。应用程序要使用某个构件,总要先弄清构件的属性、方法和事件,然后给构件属性赋值,在适当的时候调用适当的构件方法,还要给事件编写处理例程,以备构件代码来调用。何谓事件?它不过是一个指向事件例程的地址,与回调函数地址没什么区别。
    不过,此种回调方式比传统回调函数要高明许多。首先,它把让人不太舒服的回调函数变成一种自然而然的处理例程,使编程者顿觉气顺。再者,地址是一个危险的东西,用好了可使程序加速,用不好处处是陷阱,程序随时都会崩溃。现代编程方式总是想法把地址隐藏起来(隐藏比较彻底的如 VB 和 Java),其代价是降低了程序效率。事件例程使编程者无需直接操作地址,但并不会使程序减速。更妙的是,此一改变,本是有损程序结构之奇技怪巧变成一种崭新设计理念,不仅免去被人抨击,而且逼得吾等凡人净手更衣,细细研读,仰慕至今。只是偶然静心思虑,发觉不过一瓶旧酒而已,故引得此番议论,让诸君见笑 了。 事件驱动程序设计是围绕着消息基础形成的,发生一个事件,伴随着一大堆的消息。
    我理解“回调机制”是window 在执行某个API函数的过程中,调用指定的一个函数。我们可以模拟一下:
假设 ms 提供一个函数叫做 EnumFont ,该函数是得到所有的字体,假设它的实现是

EnumFont()
{
    while ( (f =FindNextFont()) !=NULL)
    {
        printf("fontname: " + f.name);
    }
}

    这样就循环显示出所有的字体名称。但是,开发者可能对字体信息另有用处,那么如何才能让开发者能使用这些信息呢,于是做改进:
EnumFont( void* userFunc )
{
    while ( (f =FindNextFont()) !=NULL)
    {
        printf("fontname: " +f.name);
        if ( userFunc!=NULL) userFunc( f);
    }
}

    假设userFunc 是一个函数 void f( FontObject font).这样使用者只需要定义一个函数:
void myfunc( FontObject font)
{
     listCtrl.Addstring ( font.name);
}

    通过使用 EnumFont( myfunc) 就可以将所有额字体信息添加到一个列表框中。那么我们称 myfunc是一个回调函数,即 让某个系统函数调用的函数。因此可以得出结论:
1 回调函数是由开发者按照一定的原型进行定义的函数
2
回调函数并不由发者直接调用执行
3 回调函数通常作为参数传递给系统API,由该API来调用。
4 回调函数可能被系统API调用一次,也可能被循环调用多次。

比如 函数

int EnumFontFamilies(
HDC hdc,     // handle to device control
LPCTSTR lpszFamily,     // pointer tofamily-name string
FONTENUMPROC lpEnumFontFamProc,    //pointer to callback function
LPARAM lParam // pointer to application-supplied data
);

    其中的FONTENUMPROC lpEnumFontFamProc就是一个回调函数,该函数遵照格式:
int CALLBACK EnumFontFamProc( ENUMLOGFONT FAR *lpelf, NEWTEXTMETRIC FAR *lpntm,int FontType, LPARAM lParam )进行定义。
    如 同mutant所说, 回调函数主要用于一些比较费时的操作,或响应不知道何时将会发生的事件,回调函数提供了一种异步的机制,相对于同步执行,提高了效率.前者的例子如WriteFileEx,ReadFileEx等,函数的最后一个参数是一个回调函数的指针,程序中调用WriteFileEx以后,就直接返回了,可以继续进行其他工作,系统在读写操作完成后通知程序作善后处理.后者的例子就是windows的事件机制回调函数的另一个用途,是用于一些 枚举函数,如EnumDisplayModes等,每找到一种支持的显示模式,就通知回调函数,由回调函数具体处理,这是因为EnumDisplayModes本身并不知道用户要如何处理.能,用户提供回调函数,定制系统的功能,这样,不同的用户提供不同的回调函数,可以使系统 具有不同的功能.这就是所谓的plugin.使用回调函数实际上就是在调用某个函数(通常是API函数)时,将自己的一个函数(这个函数为回调函数)的地址作为参数传递给那个函数。而那个函数在需要的时候,利用传递的地址调用回调函数,这时你可以利用这个机会在回调函数中处理消息或完成一定的操作。至于如 何定义回调函数,跟具体使用的API函数有关,一般在帮助中有说明回调函数的参数和返回值等。

 

 

凡是由你设计却由windows系统呼叫的函数,统称为callback函数。某些API函数要求以callback作为你参数之一。如 SetTimer,LineDDA,EnumObjects。

   回调函数是由开发者按照一定的原形进行定义的函数(每个回调函数都必须遵循这个原则来设计)

例如:

----------------------------------------
BOOL CALLBACK DialogProc(
    
     HWND hwndDlg, // handle of dialog box
     UINT uMsg, // message
     WPARAM wParam, // first message parameter
     LPARAM lParam // second message parameter
     );
----------------------------------------

说 明:
回调函数必须有关键词 CALLBACK;
回调函数本身必须是全局函数 或者静态函数,不可定义为某个特定的类的成员函数

2回调函数并不由开发者直接调用执行(只是使用系统接口API函数作为起点)
3回调函数通常作为参数传递给系统API,由该API来调用
4回调函数可能被系统API调用一次,也可能被循环调用多次

 

示范EnumObjects,发现某个Device Context的GDI obect 符合我们的形态时,呼叫callback函数.

假设我们有一个CMycalss如下:

class CMyclass {
private :
  int nCount;
  int CALLBACK _export
  EnumObjectsProc(LPSTR lpLogObject, LPSTR lpData);
public :
  void enumIt(CDC& dc);
}
void CMyclass::enumIt(CDC& dc)
{
  // 註冊 callback 函式
  dc.EnumObjects(OBJ_BRUSH, EnumObjectsProc, NULL);
}


C++编译器针对CMyclass::enumIt实际作出的码相当于:

void CMyclass::enumIt(CDC& dc)
{
  // 註冊 callback 函式
  CDC::EnumObjects(OBJ_BRUSH, EnumObjectsProc, NULL, (CDC *)&dc);
}

你所看到的最后一个参数其实是this指针,类的成员函数靠着this指针才得以抓到正确对象资料. 而nCount = 0;其实是this->nCount= 0; 基于相同的道理,上例中的EnumObjectProc既然是一个成员函数,C++编译器也会为它多准备一个隐藏参数.问题出现,  callback函数给windows呼叫用的,windows并不经由任何对象呼叫这个函数,也就无需传递this指针给callback函数,也是导致堆栈中有一个随机参数会成为this指针,而其结果当然是程序的崩溃了.

因此要把某个函数作为callback函数,就必须告诉C++编译器,不要放this指针作为该函数的最后一个参数,两个方法可以做到这一点,

1 .不要使用类的成员函数(也就是说 要使用全局函数) 作为callback函数.

2. 使用static成员函数,也就是在函数前加上static修饰词.

第一种做法相当于在C语言中使用callback函数,第二种做法接近OO精神.进一步而言,C++ 中的static函数特性是,即使对象还没有产生,static成员已经存在(函数或参数都如此).换句话说,物件产生之前你已经可以呼叫类的 static函数或者使用类的static变量了;也就是说凡是宣告为static的东西,(不管函数或变量)都并不和对象结合在一起,它们是类的一部 分,不属于对象

Q 回调函数和钩子函数有何不同?

A:  使用回调函数实际上就是在调用某个函数(通常是API函数)时,将自己的一个函数(这个函数为回调函数)的地址作为参数传递给那个函数。而那个函数在需要的时候,利用传递的地址调用回调函数,这时你可以利用这个机会在回调函数中处理消息或完成一定的操作。至于如何定义回调函数,跟具体使用的API函数有 关,一般在帮助中有说明回调函数的参数和返回值等。C++中一般要求在回调函数前加CALLBACK,这主要是说明该函数的调用方式。 
  至于钩子函数,只是回调函数的一个特例习惯上把与SetWindowsHookEx函数一起使用的回调函数 称为钩子函数。也有人把利用VirtualQueryEx安装的函数称为钩子函数,不过这种叫法不太流行。
    
    frank的意见: 
   回调函数就是一个处理函数,由系统在符合你设定的条件时自动调用。为此,你需要做三件事:1,声明;2,定义;3,设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用。
    声明和定义时应注意:回调函数由系统调用。不要把它当作你的某个类的成员函数。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值