??xml version="1.0" encoding="utf-8" standalone="yes"?> 调用U定
?br>
对于很多初学者来_往往觉得回调函数很神U,很想知道回调函数的工作原理。本文将要解释什么是回调函数、它们有什么好处、ؓ什么要使用它们{等问题Q在开始之前,假设你已l熟知了函数指针?
什么是回调函数Q?/strong>
而言之,回调函数是一个通过函数指针调用的函数。如果你把函数的指针Q地址Q作为参C递给另一个函敎ͼ当这个指针被用ؓ调用它所指向的函数时Q我们就说这是回调函数?br>
Z么要使用回调函数Q?/strong>
因ؓ可以把调用者与被调用者分开。调用者不兛_谁是被调用者,所有它需知道的,只是存在一个具有某U特定原型、某些限制条Ӟ如返回gؓintQ的被调用函数?br>
如果想知道回调函数在实际中有什么作用,先假设有q样一U情况,我们要编写一个库Q它提供了某些排序算法的实现Q如冒排序、快速排序、shell排序、shake排序{等Q但Z库更加通用Q不惛_函数中嵌入排序逻辑Q而让使用者来实现相应的逻辑Q或者,惌库可用于多种数据cdQint、float、stringQ,此时Q该怎么办呢Q可以用函数指针,q进行回调?br>
回调可用于通知机制Q例如,有时要在E序中设|一个计时器Q每C定时_E序会得到相应的通知Q但通知机制的实现者对我们的程序一无所知。而此Ӟ需有一个特定原型的函数指针Q用q个指针来进行回调,来通知我们的程序事件已l发生。实际上QSetTimer() API使用了一个回调函数来通知计时器,而且Q万一没有提供回调函数Q它q会把一个消息发往E序的消息队列?br>
另一个用回调机制的API函数是EnumWindow()Q它枚D屏幕上所有的层H口Qؓ每个H口调用一个程序提供的函数Qƈ传递窗口的处理E序。如果被调用者返回一个|ql进行P代,否则Q退出。EnumWindow()q不兛_被调用者在何处Q也不关心被调用者用它传递的处理E序做了什么,它只兛_q回|因ؓZq回|它将l箋执行或退出?br>
不管怎么_回调函数是l自C语言的,因而,在C++中,应只在与C代码建立接口Q或与已有的回调接口打交道时Q才使用回调函数。除了上q情况,在C++中应使用虚拟Ҏ或函数符QfunctorQ,而不是回调函数?br>
一个简单的回调函数实现
下面创徏了一个sort.dll的动态链接库Q它导出了一个名为CompareFunction的类?-typedef int (__stdcall *CompareFunction)(const byte*, const byte*)Q它是回调函数的类型。另外,它也导出了两个方法:Bubblesort()和Quicksort()Q这两个Ҏ原型相同Q但实现了不同的排序法?br>
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);
q两个函数接受以下参敎ͼ
·byte * arrayQ指向元素数l的指针QQ意类型)?br>
·int sizeQ数l中元素的个数?br>
·int elem_sizeQ数l中一个元素的大小Q以字节为单位?br>
·CompareFunction cmpFuncQ带有上q原型的指向回调函数的指针?br>
q两个函数的会对数组q行某种排序Q但每次都需军_两个元素哪个排在前面Q而函C有一个回调函敎ͼ其地址是作Z个参C递进来的。对~写者来_不必介意函数在何处实玎ͼ或它怎样被实现的Q所需在意的只是两个用于比较的元素的地址Qƈq回以下的某个|库的~写者和使用者都必须遵守q个U定Q:
·-1Q如果第一个元素较,那它在已排序好的数组中,应该排在W二个元素前面?br>
·0Q如果两个元素相{,那么它们的相对位|ƈ不重要,在已排序好的数组中,谁在前面都无所谓?
·1Q如果第一个元素较大,那在已排序好的数l中Q它应该排第二个元素后面?br>
Z以上U定Q函数Bubblesort()的实现如下,Quicksort()q微复杂一点:
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))
{
//两个相比较的元素怺?br> 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;
}
}
}
}
注意Q因为实C使用了memcpy()Q所以函数在使用的数据类型方面,会有所局限?br>
对用者来_必须有一个回调函敎ͼ其地址要传递给Bubblesort()函数。下面有二个单的CZQ一个比较两个整敎ͼ而另一个比较两个字W串Q?br>
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);
}
下面另有一个程序,用于试以上所有的代码Q它传递了一个有5个元素的数组lBubblesort()和Quicksort()Q同时还传递了一个指向回调函数的指针?br>
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;
}
如果惌行降序排序(大元素在先)Q就只需修改回调函数的代码,或用另一个回调函敎ͼq样~程h灉|性就比较大了?/p>
上面的代码中Q可在函数原型中扑ֈ__stdcallQ因为它以双下划U打_所以它是一个特定于~译器的扩展Q说到底也就是微软的实现。Q何支持开发基于Win32的程序都必须支持q个扩展或其{h物。以__stdcall标识的函C用了标准调用U定Qؓ什么叫标准U定呢,因ؓ所有的Win32 APIQ除了个别接受可变参数的除外Q都使用它。标准调用约定的函数在它们返回到调用者之前,都会从堆栈中U除掉参敎ͼq也是Pascal的标准约定。但在C/C++中,调用U定是调用者负责清理堆栈,而不是被调用函数Qؓ强制函数使用C/C++调用U定Q可使用__cdecl。另外,可变参数函数也用C/C++调用U定?br>
Windows操作pȝ采用了标准调用约定(PascalU定Q,因ؓ其可减小代码的体U。这点对早期的Windows来说非常重要Q因为那时它q行在只?40KB内存的电脑上?br>
如果你不喜欢__stdcallQ还可以使用CALLBACK宏,它定义在windef.h中:
#define CALLBACK __stdcallor
#define CALLBACK PASCAL //而PASCAL在此?defined成__stdcall
作ؓ回调函数的C++Ҏ
因ؓqx很可能会使用到C++~写代码Q也怼惛_把回调函数写成类中的一个方法,但先来看看以下的代码Q?br>
class CCallbackTester
{
public:
int CALLBACK CompareInts(const byte* velem1, const byte* velem2);
};
Bubblesort((byte*)array, 5, sizeof(array[0]),
&CCallbackTester::CompareInts);
如果使用微Y的编译器Q将会得C面这个编译错误:
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
q是因ؓ非静态成员函数有一个额外的参数Qthis指针Q这迫使你在成员函数前面加上static。当Ӟq有几种Ҏ可以解决q个问题Q但限于幅Q就不再了?br>
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
提出问题Q?/font>
回调函数是基于C~程的Windows SDK的技术,不是针对C++的,E序员可以将一个C函数直接作ؓ回调函数Q但是如果试囄接用C++的成员函C为回调函数将发生错误Q甚至编译就不能通过?br>分析原因Q?/font>
普通的C++成员函数都隐含了一个传递函C为参敎ͼ亦即“this”指针QC++通过传递一个指向自w的指针l其成员函数从而实现程序函数可以访问C++的数据成员。这也可以理解ؓ什么C++cȝ多个实例可以׃n成员函数但是有不同的数据成员。由于this指针的作用,使得一个CALLBACK型的成员函数作ؓ回调函数安装时就会因为隐含的this指针使得函数参数个数不匹配,从而导致回调函数安装失?br>解决ҎQ?/font>
一,不用成员函敎ͼ直接使用普通C函数Qؓ了实现在C函数中可以访问类的成员变量,可以使用友元操作W?friend)Q在C++中将该C函数说明?font color=#ff1493>cȝ友元卛_。这U处理机制与普通的C~程中用回调函C栗?br>?使用静态成员函?/font>Q静态成员函C使用this指针作ؓ隐含参数Q这样就可以作ؓ回调函数了。静态成员函数具有两大特点:其一Q可以在没有cd例的情况下用;其二Q只能访问静态成员变量和静态成员函敎ͼ不能讉K非静态成员变量和非静态成员函数。由于在C++中用类成员函数作ؓ回调函数的目的就是ؓ了访问所有的成员变量和成员函敎ͼ如果作不到这一点将不具有实际意义。我们通过使用静态成员函数对非静态成员函?font color=#ff1493>包装的办法来解决问题。类实例可以通过附加参数?font color=#ff1493>全局变量的方式的方式传递到静态成员函C。分别D例如下:
注意Q通过上面两种Ҏ的比较可以看出,W?U方法中静态包装函数可以和普通成员函C持签名一_当回调函数的宿主接口不能改变Ӟq种Ҏ特别有用。但因ؓ使用了全局变量Q也不是一个好的设计?/p>
下面我们分别详细介绍Q?br> 1./Gd
q是~译器默认的转换模式Q对一般函C?C的函数调用{换方式__cdeclQ但是对于C++ 成员函数和前面修C__stdcall __fastcall的函数除外?br> 2./Gr
对于一般函C用__fastcall函数调用转换方式Q所有用__fastcall的函数必要有函数原形。但对于C++ 成员函数和前面修C__cdecl __stdcall 的函数除外?br> 3./Gz
对于所?C函数使用__stdcall函数调用转换方式Q但对于可变参数?C函数以及用__cdecl __fastcall修饰q的函数和C++ 成员函数除外。所有用__stdcall修饰的函数必L函数原Ş?br> 事实上,对于x86pȝQC++ 成员函数的调用方式有点特别,成员函数的this
指针攑օECXQ所有函数参C叛_左入栈,被调用的成员函数负责清理入栈?br>参数。对于可变参数的成员函数Q始l用__cdecl的{换方式?br> 下面该进入主题,分别讲一下这三种函数调用转换方式有什么区别:
1.__cdecl
q是~译器默认的函数调用转换方式Q它可以处理可变参数的函数调用。参数的入栈序是从叛_左。在函数q行l束后,p用函数负责清理入栈的参数。在~译Ӟ在每个函数前面加上下划线(_)Q没有函数名大小写的转换。即_functionname
每一个调用它的函数都包含清空堆栈的代码,所以生的可执行文件大会比调用_stdcall函数的大。函数采用从叛_左的压栈方式。注意:对于可变参数的成员函敎ͼ始终使用__cdecl的{换方式?
2.__fastcall
有一些函数调用的参数被放入ECXQEDX中,而其它参C叛_左入栈。被调用函数在它要q回时负责清理入栈的参数。在内嵌汇编语言的时候,需要注意寄存器的用,以免与编译器使用的生冲H。函数名字的转换是:@functionname@number没有函数名大写的{换,number表示函数参数的字节数。由于有一些参C 需要入栈,所以这U{换方式会在一定程度上提高函数调用的速度?br> 3.__stdcall
函数参数从右向左入栈Q被调用函数负责入栈参数的清理工作。函数名转换格式如下Q?a href="mailto:_functionname@number">_functionname@number
下面我们亲自写一个程序,看看各种不同的调用在~译后有什么区别,我们的被?nbsp;用函数如下:
1.__cdecl
_function
push ebp
mov ebp, esp
mov eax, [ebp+8] ;参数1
add eax, [ebp+C] ;加上参数2
pop ebp
retn
_main
push ebp
mov ebp, esp
push 14h ;参数 2入栈
push 0Ah ;参数 1入栈
call _function ;调用函数
add esp, 8 ;修正?br>xor eax, eax
pop ebp
retn
2.__fastcall
@function@8
push ebp
mov ebp, esp ;保存栈指?br>sub esp, 8 ;多了两个局部变?br>mov [ebp-8], edx ;保存参数 2
mov [ebp-4], ecx ;保存参数 1
mov eax, [ebp-4] ;参数 1
add eax, [ebp-8] ;加上参数 2
mov esp, ebp ;修正?br>pop ebp
retn
_main
push ebp
mov ebp, esp
mov edx, 14h ;参数 2lEDX
mov ecx, 0Ah ;参数 1lECX
call @function@8 ;调用函数
xor eax, eax
pop ebp
retn
3.__stdcall
_function@8
push ebp
mov ebp, esp
mov eax, [ebp] ;参数 1
add eax, [ebp+C] ;加上参数 2
pop ebp
retn 8 ;修复?br>_main
push ebp
mov ebp, esp
push 14h ;参数 2入栈
push 0Ah ;参数 1入栈
call _function@8 ;函数调用
xor eax, eax
pop ebp
retn
可见上述三种Ҏ各有各的特点Q而且_main必须是__cdeclQ一般WIN32的函数都是__stdcall。而且在Windef.h中有如下的定义:
#define CALLBACK __stdcall
#define WINAPI __stdcall
补:
C++~译时函数名修饰U定规则Q?
__stdcall调用U定Q?
1)、以"?"标识函数名的开始,后跟函数名;
2)、函数名后面?@@YG"标识参数表的开始,后跟参数表;
3)、参数表以代可C:
X--void Q?
D--charQ?
E--unsigned charQ?
F--shortQ?
H--intQ?
I--unsigned intQ?
J--longQ?
K--unsigned longQ?
M--floatQ?
N--doubleQ?
_N--boolQ?
PA--表示指针Q后面的代号表明指针cdQ如果相同类型的指针q箋出现Q以"0"代替Q一?0"代表一ơ重复;
4)、参数表的第一ؓ该函数的q回值类型,其后依次为参数的数据cd,指针标识在其所指数据类型前Q?
5)、参数表后以"@Z"标识整个名字的结束,如果该函数无参数Q则?Z"标识l束?
其格式ؓ"?functionname@@YG*****@Z"??functionname@@YG*XZ"Q例? int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z” void Test2() -----“?Test2@@YGXXZ”
__cdecl调用U定Q?
规则同上面的_stdcall调用U定Q只是参数表的开始标识由上面?@@YG"变ؓ"@@YA"?
__fastcall调用U定Q?
规则同上面的_stdcall调用U定Q只是参数表的开始标识由上面?@@YG"变ؓ"@@YI"?
VC++对函数的省缺声明?__cedcl",只能被C/C++调用.
注意Q?
1、_beginthread需要__cdecl的线E函数地址Q_beginthreadex和CreateThread需要__stdcall的线E函数地址?
2、一般WIN32的函数都是__stdcall。而且在Windef.h中有如下的定义:
#define CALLBACK __stdcall
#define WINAPI __stdcall
3、extern "C" _declspec(dllexport) int __cdecl Add(int a, int b);
typedef int (__cdecl*FunPointer)(int a, int b);
修饰W的书写序如上?
4?extern "C"的作用:如果Add(int a, int b)是在c语言~译器编译,而在c++文g使用Q则需要在c++文g中声明:extern "C" Add(int a, int b)Q因为c~译器和c++~译器对函数名的解释不一Pc++~译器解释函数名的时候要考虑函数参数Q这h了方便函数重载,而在c语言中不存在函数?载的问题Q,使用extern "C"Q实质就是告诉c++~译器,该函数是c库里面的函数。如果不使用extern "C"则会出现链接错误?
一般象如下使用Q?
#ifdef _cplusplus
#define EXTERN_C extern "C"
#else
#define EXTERN_C extern
#endif
#ifdef _cplusplus
extern "C"{
#endif
EXTERN_C int func(int a, int b);
#ifdef _cplusplus
}
#endif
5、MFC提供了一些宏Q可以用AFX_EXT_CLASS来代替__declspec(DLLexport)Qƈ修饰cdQ从而导出类QAFX_API_EXPORT来修饰函敎ͼAFX_DATA_EXPORT来修饰变?
AFX_CLASS_IMPORTQ__declspec(DLLexport)
AFX_API_IMPORTQ__declspec(DLLexport)
AFX_DATA_IMPORTQ__declspec(DLLexport)
AFX_CLASS_EXPORTQ__declspec(DLLexport)
AFX_API_EXPORTQ__declspec(DLLexport)
AFX_DATA_EXPORTQ__declspec(DLLexport)
AFX_EXT_CLASSQ?ifdef _AFXEXT
AFX_CLASS_EXPORT
#else
AFX_CLASS_IMPORT
6?DLLMain负责初始?Initialization)和结?Termination)工作Q每当一个新的进E或者该q程的新的线E访问DLLӞ 或者访问DLL的每一个进E或者线E不再用DLL或者结束时Q都会调用DLLMain。但是,使用TerminateProcess?TerminateThreadl束q程或者线E,不会调用DLLMain?
7、一个DLL在内存中只有一个实?
DLLE序和调用其输出函数的程序的关系Q?
1)、DLL与进E、线E之间的关系
DLL模块被映到调用它的q程的虚拟地址I间?
DLL使用的内存从调用q程的虚拟地址I间分配Q只能被该进E的U程所讉K?
DLL的句柄可以被调用q程使用Q调用进E的句柄可以被DLL使用?
DLLDLL可以有自q数据D,但没有自q堆栈Q用调用进E的栈,与调用它的应用程序相同的堆栈模式?
2)、关于共享数据段
DLL 定义的全局变量可以被调用进E访问;DLL可以讉K调用q程的全局数据。用同一DLL的每一个进E都有自qDLL全局变量实例。如果多个线Eƈ发访?同一变量Q则需要用同步机Ӟ对一个DLL的变量,如果希望每个使用DLL的线E都有自q|则应该用线E局部存?TLSQThread Local Strorage)
初学MFC的一个小l习 ..
下面留个链接 .供以后回?..~