什么是回调函数

[   原创文档   本文适合中级读者   已阅读7366次   ]  
   
   
  声明函数指针并实现回调  
  作者:Danny   Kalev    
  编译:赵湘宁    
  原文出处:Declaring   Function   Pointers   and   Implementing   Callbacks      
   
  --------------------------------------------------------------------------------  
     
          程序员常常需要实现回调。本文将讨论函数指针的基本原则并说明如何使用函数指针实现回调。注意这里针对的是普通的函数,不包括完全依赖于不同语法和语义规 则的类成员函数(类成员指针将在另文中讨论)。  
   
  声明函数指针  
   
          回调函数是一个程序员不能显式调用的函数;通过将回调函数的地址传给调用者从而实现调用。要实现回调,必须首先定义函数指针。尽管定义的语法有点不可思 议,但如果你熟悉函数声明的一般方法,便会发现函数指针的声明与函数声明非常类似。请看下面的例子:  
   
  void   f();//   函数原型  
   
  上面的语句声明了一个函数,没有输入参数并返回void。那么函数指针的声明方法如下:  
   
  void   (*)   ();  
   
          让我们来分析一下,左边圆括弧中的星号是函数指针声明的关键。另外两个元素是函数的返回类型(void)和由边圆括弧中的入口参数(本例中参数是空)。注 意本例中还没有创建指针变量-只是声明了变量类型。目前可以用这个变量类型来创建类型定义名及用sizeof表达式获得函数指针的大小:  
   
  //   获得函数指针的大小  
  unsigned   psize   =   sizeof   (void   (*)   ());    
   
  //   为函数指针声明类型定义  
  typedef   void   (*pfv)   ();  
   
  pfv是一个函数指针,它指向的函数没有输入参数,返回类行为void。使用这个类型定义名可以隐藏复杂的函数指针语法。  
   
  指针变量应该有一个变量名:  
   
  void   (*p)   ();   //p是指向某函数的指针  
   
          p是指向某函数的指针,该函数无输入参数,返回值的类型为void。左边圆括弧里星号后的就是指针变量名。有了指针变量便可以赋值,值的内容是署名匹配的 函数名和返回类型。例如:  
   
  void   func()    
  {  
  /*   do   something   */  
  }    
  p   =   func;    
   
  p的赋值可以不同,但一定要是函数的地址,并且署名和返回类型相同。  
   
  传递回调函数的地址给调用者  
   
          现在可以将p传递给另一个函数(调用者)-   caller(),它将调用p指向的函数,而此函数名是未知的:  
   
  void   caller(void(*ptr)())  
  {  
  ptr();   /*   调用ptr指向的函数   */    
  }  
  void   func();  
  int   main()  
  {  
  p   =   func;    
  caller(p);   /*   传递函数地址到调用者   */  
  }  
   
          如果赋了不同的值给p(不同函数地址),那么调用者将调用不同地址的函数。赋值可以发生在运行时,这样使你能实现动态绑定。  
   
  调用规范  
   
          到目前为止,我们只讨论了函数指针及回调而没有去注意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,尽管两者有相同的返回值和参数列。    
   
    __________________________________________________________________________
   
   
   
  回调函数、消息和事件例程  
   
   
   
          调用(calling)机制从汇编时代起已经大量使用:准备一段现成的代码,调用者可以随时跳转至此段代码的起始地址,执行完后再返回跳转时的后续地址。 CPU为此准备了现成的调用指令,调用时可以压栈保护现场,调用结束后从堆栈中弹出现场地址,以便自动返回。借堆栈保护现场真是一项绝妙的发明,它使调用 者和被调者可以互不相识,于是才有了后来的函数和构件,使吾辈编程者如此轻松愉快。若评选对人类影响最大之发明,在火与车轮之后,笔者当推压栈调用。  
          话虽这样说,此调用机制并非完美。回调函数就是一例。函数之类本是为调用者准备的美餐,其烹制者应对食客了如指掌,但实情并非如此。例如,写一个快速排序 函数供他人调用,其中必包含比较大小。麻烦来了:此时并不知要比较的是何类数据--整数、浮点数、字符串?于是只好为每类数据制作一个不同的排序函数。更 通行的办法是在函数参数中列一个回调函数地址,并通知调用者:君需自己准备一个比较函数,其中包含两个指针类参数,函数要比较此二指针所指数据之大小,并 由函数返回值说明比较结果。排序函数借此调用者提供的函数来比较大小,借指针传递参数,可以全然不管所比较的数据类型。被调用者回头调用调用者的函数(够 咬嘴的),故称其为回调(callback)。  
          回调函数使程序结构乱了许多。Windows   API   函数集中有不少回调函数,尽管有详尽说明,仍使初学者一头雾水。恐怕这也是无奈之举。无论何种事物,能以树形结构单向描述毕竟让人舒服些。如果某家族中孙 辈又是某祖辈的祖辈,恐怕无人能理清其中的头绪。但数据处理之复杂往往需要构成网状结构,非简单的客户/服务器关系能穷尽。  
          Windows   系统还包含着另一种更为广泛的回调机制,即消息机制。消息本是   Windows   的基本控制手段,乍看与函数调用无关,其实是一种变相的函数调用。发送消息的目的是通知收方运行一段预先准备好的代码,相当于调用一个函数。消息所附带的   WParam   和   LParam   相当于函数的参数,只不过比普通参数更通用一些。应用程序可以主动发送消息,更多情况下是坐等   Windows   发送消息。一旦消息进入所属消息队列,便检感兴趣的那些,跳转去执行相应的消息处理代码。操作系统本是为应用程序服务,由应用程序来调用。而应用程序一旦 启动,却要反过来等待操作系统的调用。这分明也是一种回调,或者说是一种广义回调。其实,应用程序之间也可以形成这种回调。假如进程   B   收到进程   A   发来的消息,启动了一段代码,其中又向进程   A   发送消息,这就形成了回调。这种回调比较隐蔽,弄不好会搞成递归调用,若缺少终止条件,将会循环不已,直至把程序搞垮。若是故意编写成此递归调用,并设好 终止条件,倒是很有意思。但这种程序结构太隐蔽,除非十分必要,还是不用为好。  
          利用消息也可以构成狭义回调。上面所举排序函数一例,可以把回调函数地址换成窗口   handle。如此,当需要比较数据大小时,不是去调用回调函数,而是借   API   函数   SendMessage   向指定窗口发送消息。收到消息方负责比较数据大小,把比较结果通过消息本身的返回值传给消息发送方。所实现的功能与回调函数并无不同。当然,此例中改为消 息纯属画蛇添脚,反倒把程序搞得很慢。但其他情况下并非总是如此,特别是需要异步调用时,发送消息是一种不错的选择。假如回调函数中包含文件处理之类的低 速处理,调用方等不得,需要把同步调用改为异步调用,去启动一个单独的线程,然后马上执行后续代码,其余的事让线程慢慢去做。一个替代办法是借   API   函数   PostMessage   发送一个异步消息,然后立即执行后续代码。这要比自己搞个线程省事许多,而且更安全。  
          如今我们是活在一个   object   时代。只要与编程有关,无论何事都离不开   object。但   object   并未消除回调,反而把它发扬光大,弄得到处都是,只不过大都以事件(event)的身份出现,镶嵌在某个结构之中,显得更正统,更容易被人接受。应用程序 要使用某个构件,总要先弄清构件的属性、方法和事件,然后给构件属性赋值,在适当的时候调用适当的构件方法,还要给事件编写处理例程,以备构件代码来调 用。何谓事件?它不过是一个指向事件例程的地址,与回调函数地址没什么区别。  
          不过,此种回调方式比传统回调函数要高明许多。首先,它把让人不太舒服的回调函数变成一种自然而然的处理例程,使编程者顿觉气顺。再者,地址是一个危险的 东西,用好了可使程序加速,用不好处处是陷阱,程序随时都会崩溃。现代编程方式总是想法把地址隐藏起来(隐藏比较彻底的如   VB   和   Java),其代价是降低了程序效率。事件例程使编程者无需直接操作地址,但并不会使程序减速。更妙的是,此一改变,本是有损程序结构之奇技怪巧变成一种 崭新设计理念,不仅免去被人抨击,而且逼得吾等凡人净手更衣,细细研读,仰慕至今。只是偶然静心思虑,发觉不过一瓶旧酒而已,故引得此番议论,让诸君见笑 了。 



coder_jack@126.com日记写到200902061836

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值