C++回调函数的定义与⽤法
⼀回调函数
我们经常在C++设计时通过使⽤回调函数可以使有些应⽤(如定时器事件回调处理、⽤回调函数记录某操作进度等)变得⾮常⽅便和符合逻辑,那么它的内在机制如何呢,怎么定义呢?它和其它函数(⽐如钩⼦函数)有何不同呢?
使⽤回调函数实际上就是在调⽤某个函数(通常是API函数)时,将⾃⼰的⼀个函数(这个函数为回调函数)的地址作为参数传递给那个函数。
⽽ 那个函数在需要的时候,利⽤传递的地址调⽤回调函数,这时你可以利⽤这个机会在回调函数中处理消息或完成⼀定的操作。⾄于如何定义回调函数,跟具体使⽤的 API函数有关,⼀般在帮助中有说明回调函数的参数和返回值等。C++中⼀般要求在回调函数前加CALLBACK(相当于FAR PASCAL),这主要是说明该函数的调⽤⽅式。
⾄于钩⼦函数,只是回调函数的⼀个特例。习惯上把与SetWindowsHookEx函数⼀起使⽤的回调函数称为钩⼦函数。也有⼈把利⽤VirtualQueryEx安装的函数称为钩⼦函数,不过这种叫法不太流⾏。
也可以这样,更容易理解:回调函数就好像是⼀个中断处理函数,系统在符合你设定的条件时⾃动调⽤。
为此,你需要做三件事:
1. 声明;
2. 定义;
3. 设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为⼀个参数,以便于系统调⽤。
声明和定义时应注意:回调函数由系统调⽤,所以可以认为它属于WINDOWS系统,不要把它当作你的某个类的成员函数。
⼆回调函数、消息和事件例程
调⽤(calling)机制从汇编时代起已经⼤量使⽤:准备⼀段现成的代码,调⽤者可以随时跳转⾄此段代码的起始地址,执⾏完后再返回跳转时的后续地址。 CPU为此准备了现成的调⽤指令,调⽤时可以压栈保护现场,调⽤结束后从堆栈中弹出现场地址,以便⾃动返回。借堆栈保护现场真是⼀项绝妙的发明,它使调⽤ 者和被调者可以互不相识,于是才有了后来的函数和构件。
此调⽤机制并⾮完美。回调函数就是⼀例。函数之类本是为调⽤者准备的美餐,其烹制者应对⾷客了如
指掌,但实情并⾮如此。例如,写⼀个快速排序函数供他⼈调 ⽤,其中必包含⽐较⼤⼩。⿇烦来了:此时并不知要⽐较的是何类数据--整数、浮点数、字符串?于是只好为每类数据制作⼀个不同的排序函数。更通⾏的办法是 在函数参数中列⼀个回调函数地址,并通知调⽤者:君需⾃⼰准备⼀个⽐较函数,其中包含两个指针类参数,函数要⽐较此⼆指针所指数据之⼤⼩,并由函数返回值 说明⽐较结果。排序函数借此调⽤者提供的函数来⽐较⼤⼩,借指针传递参数,可以全然不管所⽐较的数据类型。被调⽤者回头调⽤调⽤者的函数(够咬嘴的),故 称其为回调(callback)。
回调函数使程序结构乱了许多。Windows API 函数集中有不少回调函数,尽管有详尽说明,仍使初学者⼀头雾⽔。恐怕这也是⽆奈之举。
⽆论何种事物,能以树形结构单向描述毕竟让⼈舒服些。如果某家族中孙辈⼜是某祖辈的祖辈,恐怕⽆⼈能理清其中的头绪。但数据处理之复杂往往需要构成⽹状结构,⾮简单的客户/服务器关系能穷尽。
Windows 系统还包含着另⼀种更为⼴泛的回调机制,即消息机制。消息本是 Windows 的基本控制⼿段,乍看与函数调⽤⽆关,其实是⼀种变相的函数调⽤。发送消息的⽬的是通知收⽅运⾏⼀段预先准备好的代码,相当于调⽤⼀个函数。消息所附带的 WParam 和LParam 相当于函数的参数,只不过⽐普通参数更通⽤⼀些。应⽤程序可以主动发送消息,更多情况下是坐等 Windows 发送消息。⼀旦消息进⼊所属消息队列,便检感兴趣的那些,跳转去执⾏相应的消息处理代码。操作系统本是为应⽤程序服务,
由应⽤程序来调⽤。⽽应⽤程序⼀旦 启动,却要反过来等待操作系统的调⽤。这分明也是⼀种回调,或者说是⼀种⼴义回调。其实,应⽤程序之间也可以形成这种回调。假如进程 B 收到进程 A 发来的消息,启动了⼀段代码,其中⼜向进程 A 发送消息,这就形成了回调。这种回调⽐较隐蔽,弄不好会搞成递归调⽤,若缺少终⽌条件,将会循环不已,直⾄把程序搞垮。若是故意编写成此递归调⽤,并设好 终⽌条件,倒是很有意思。但这种程序结构太隐蔽,除⾮⼗分必要,还是不⽤为好。
利⽤消息也可以构成狭义回调。上⾯所举排序函数⼀例,可以把回调函数地址换成窗⼝ handle。如此,当需要⽐较数据⼤⼩时,不是去调⽤回调函数,⽽是借 API 函数 SendMessage 向指定窗⼝发送消息。收到消息⽅负责⽐较数据⼤⼩,把⽐较结果通过消息本⾝的返回值传给消息发送⽅。所实现的功能与回调函数并⽆不同。当然,此例中改为消 息纯属画蛇添脚,反倒把程序搞得很慢。但其他情况下并⾮总是如此,特别是需要异步调⽤时,发送消息是⼀种不错的选择。假如回调函数中包含⽂件处理之类的低 速处理,调⽤⽅等不得,需要把同步调⽤改为异步调⽤,去启动⼀个单独的线程,然后马上执⾏后续代码,其余的事让线程慢慢去做。⼀个替代办法是借 API 函数PostMessage 发送⼀个异步消息,然后⽴即执⾏后续代码。这要⽐⾃⼰搞个线程省事许多,⽽且更安全。
如今我们是活在⼀个 object 时代。只要与编程有关,⽆论何事都离不开 object。但 object 并未消除回调,反⽽把它发扬光⼤,弄得到处都是,只不过⼤都以事件(event)的⾝份出现,镶嵌在某个结构之中,显得更正统,更容易被⼈接受。应⽤程序 要使⽤某个构件,总要先弄清构件的属性、⽅法和事件,
然后给构件属性赋值,在适当的时候调⽤适当的构件⽅法,还要给事件编写处理例程,以备构件代码来调 ⽤。何谓事件?它不过是⼀个指向事件例程的地址,与回调函数地址没什么区别。
不过,此种回调⽅式⽐传统回调函数要⾼明许多。⾸先,它把让⼈不太舒服的回调函数变成⼀种⾃然⽽然的处理例程,使编程者顿觉⽓顺。再者,地址是⼀个危险的 东西,⽤好了可使程序加速,⽤不好处处是陷阱,程序随时都会崩溃。现代编程⽅式总是想法把地址隐藏起来(隐藏⽐较彻底的如 VB 和 Java),其代价是降低了程序效率。事件例程(?)使编程者⽆需直接操作地址,但并不会使程序减速。
(例程似乎是进程的台湾翻译。)
三精妙⽐喻:回调函数还真有点像您随⾝带的BP机:告诉别⼈号码,在它有事情时Call您。
回调⽤于层间协作,上层将本层函数安装在下层,这个函数就是回调,⽽下层在⼀定条件下触发回调,例如作为⼀个驱动,是⼀个底层,他在收到⼀个数据时,除了 完成本层的处理⼯作外,还将进⾏回调,将这个数据交给上层应⽤层来做进⼀步处理,这在分层的数据通信中很普遍。其实回调和API⾮常接近,他们的共性都是 跨层调⽤的函数。但区别是API是低层提供给⾼层的调⽤,⼀般这个函数对⾼层都是已知的;⽽回调正好相反,他是⾼层提供给底层的调⽤,对于低层他是未知 的,必须由⾼层进⾏安装,这个安装函数其实就是⼀个低层提供的API,安装后低层不知道这个回调的名字,但它通过⼀个函vb采用什么的编程机制
数指针来保存这个回调,在需要调⽤ 时,只需引⽤这个函数指针和相关的参数指针。 其实:回调就是该函数写在⾼层,低层通过⼀个函数指针保存这个函数,在某个事件的触发下,低层通过该函数指针调⽤⾼层那个函数。
四调⽤⽅式
软件模块之间总是存在着⼀定的接⼝,从调⽤⽅式上,可以把他们分为三类:同步调⽤、回调和异步调⽤。同步调⽤是⼀种阻塞式调⽤,调⽤⽅要等待对⽅执⾏完毕 才返回,它是⼀种单向调⽤;回调是⼀种双向调⽤模式,也就是说,被调⽤⽅在接⼝被调⽤时也会调⽤对⽅的接⼝;异步调⽤是⼀种类似消息或事件的机制,不过它 的调⽤⽅向刚好相反,接⼝的服务在收到某种讯息或发⽣某种事件时,会主动通知客户⽅(即调⽤客户⽅的接⼝)。回调和异步调⽤的关系⾮常紧密,通常我们使⽤ 回调来实现异步消息的注册,通过异步调⽤来实现消息的通知。同步调⽤是三者当中最简单的,⽽回调⼜常常是异步调⽤的基础。
对于不同类型的语⾔(如结构化语⾔和对象语⾔)、平台(Win32、JDK)或构架(CORBA、DCOM、WebService),客户和服务的交互除 了同步⽅式以外,都需要具备⼀定的异步通知机制,让服务⽅(或接⼝提供⽅)在某些情况下能够主动通知客户,⽽回调是实现异步的⼀个最简捷的途径。
对于⼀般的结构化语⾔,可以通过回调函数来实现回调。回调函数也是⼀个函数或过程,不过它是⼀
个由调⽤⽅⾃⼰实现,供被调⽤⽅使⽤的特殊函数。
在⾯向对象的语⾔中,回调则是通过接⼝或抽象类来实现的,我们把实现这种接⼝的类成为回调类,回调类的对象成为回调对象。对于象C++或Object Pascal这些兼容了过程特性的对象语⾔,不仅提供了回调对象、回调⽅法等特性,也能兼容过程语⾔的回调函数机制。
Windows平台的消息机制也可以看作是回调的⼀种应⽤,我们通过系统提供的接⼝注册消息处理函数(即回调函数),从⽽实现接收、处理消息的⽬的。由于Windows平台的API是⽤C语⾔来构建的,我们可以认为它也是回调函数的⼀个特例。
对于分布式组件代理体系CORBA,异步处理有多种⽅式,如回调、事件服务、通知服务等。事件服务和通知服务是CORBA⽤来处理异步消息的标准服务,他们主要负责消息的处理、派发、维护等⼯作。对⼀些简单的异步处理过程,我们可以通过回调机制来实现。
下⾯我们集中⽐较具有代表性的语⾔(C、Object Pascal)和架构(CORBA)来分析回调的实现⽅式、具体作⽤等。
过程语⾔中的回调(C)
(1 )函数指针
回调在C语⾔中是通过函数指针来实现的,通过将回调函数的地址传给被调函数从⽽实现回调。因此,要实现回调,必须⾸先定义函数指针,请看下⾯的例⼦:
void Func(char *s);// 函数原型
void (*pFunc) (char *);//函数指针
可以看出,函数的定义和函数指针的定义⾮常类似。
⼀般的化,为了简化函数指针类型的变量定义,提⾼程序的可读性,我们需要把函数指针类型⾃定义⼀下。
typedef void(*pcb)(char *);
回调函数可以象普通函数⼀样被程序调⽤,但是只兴 坏弊鞑问 莞 坏骱 辈拍艹谱骰氐骱 ?
被调函数的例⼦:
void GetCallBack(pcb callback)
{
}
⽤户在调⽤上⾯的函数时,需要⾃⼰实现⼀个pcb类型的回调函数:
void fCallback(char *s)
{
}
然后,就可以直接把fCallback当作⼀个变量传递给GetCallBack,
GetCallBack(fCallback);
如果赋了不同的值给该参数,那么调⽤者将调⽤不同地址的函数。赋值可以发⽣在运⾏时,这样使你能实现动态绑定。
(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,尽管两者有相同的返回值和参数列
(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;
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论