什么是回调函数

 
什么是回调函数?
  简而言之,回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。
 
为什么要使用回调函数?
  因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。
 
  如果想知道回调函数在实际中有什么作用,先假设有这样一种情况,我们要编写一个库,它提供了某些排序算法的实现,如冒泡排序、快速排序、shell排序、shake排序等等,但为使库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,想让库可用于多种数据类型(int、float、string),此时,该怎么办呢?可以使用函数指针,并进行回调。
 
  回调可用于通知机制,例如,有时要在程序中设置一个计时器,每到一定时间,程序会得到相应的通知,但通知机制的实现者对我们的程序一无所知。而此时,就需有一个特定原型的函数指针,用这个指针来进行回调,来通知我们的程序事件已经发生。实际上,SetTimer() API使用了一个回调函数来通知计时器,而且,万一没有提供回调函数,它还会把一个消息发往程序的消息队列。
 
  另一个使用回调机制的API函数是EnumWindow(),它枚举屏幕上所有的顶层窗口,为每个窗口调用一个程序提供的函数,并传递窗口的处理程序。如果被调用者返回一个值,就继续进行迭代,否则,退出。EnumWindow()并不关心被调用者在何处,也不关心被调用者用它传递的处理程序做了什么,它只关心返回值,因为基于返回值,它将继续执行或退出。
 
  不管怎么说,回调函数是继续自C语言的,因而,在C++中,应只在与C代码建立接口,或与已有的回调接口打交道时,才使用回调函数。除了上述情况,在C++中应使用虚拟方法或函数符(functor),而不是回调函数。
 
============================================================================================================================================
回调函数官方说明及网友经验
网友表达一:
回调函数和其他函数没什么区别,只是有固定的参数和调用约定,比如windows的回掉函数都要用stdcall. 参数是由调用这个回调函数的函数传递的,当然要规定好参数格式。使用很简单,就是传递这个回调函数的地址给调用的函数作为一个参数。比如创建线程的时候就是这样.
 
网友表达二:
简单说就是,由程序员编写的,给windows系统调用的函数。 也就是说,函数的功能有你定,调用有windows系统调用,不用你关心。你所要做的只是把函数的参数按照规定格式写好,编写好函数。
 
官方(书面)说法:
 回调函数是一个程序员不能显式调用的函数;通过将回调函数的地址传给调用者从而实现调用。要实现回调,必须首先定义函数指针。尽管定义的语法有点不可思议,但如果你熟悉函数声明的一般方法,便会发现函数指针的声明与函数声明非常类似。请看下面的例子:
 
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,尽管两者有相同的返回值和参数列。
 
 
 
 
 
什么是回调函数
先泛后精,先浅后深 的道理可是知易行难,所以常常会给 技术 上的小细节纠缠着,搞得晕晕乎,其中 回调函数 就是其中之一了,虽然回用,但是老是不明白其中的含义,众多的书本也说得隐晦不清。直到不久前看到一篇文章才总算比较清晰了解。
 
概括起来,回调机制包括两部分:服务执行者和服务方式制定者。
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 to family-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 函数有关,一般在帮助中有说明回调函数的参数和返回值等。
 
 
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值