谈谈,回调函数

hardstudylulin 2003-11-24 03:53:12
前几天看到的文章,贴出来供大家参考!
...全文
124 17 打赏 收藏 转发到动态 举报
写回复
用AI写文章
17 条回复
切换为时间正序
请发表友善的回复…
发表回复
panzhaoping 2003-11-25
  • 打赏
  • 举报
回复
其实就是个压栈的大小问题
panzhaoping 2003-11-25
  • 打赏
  • 举报
回复
有这样复杂吗?
可以从汇编角度讲很简单的
yintongshun 2003-11-25
  • 打赏
  • 举报
回复

调用约定

调用约定(Calling convention)决定以下内容:函数参数的压栈顺序,由调用者还是被调用者把参数弹出栈,以及产生函数修饰名的方法。MFC支持以下调用约定:


_cdecl

按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于“C”函数或者变量,修饰名是在函数名前加下划线。对于“C++”函数,有所不同。

如函数void test(void)的修饰名是_test;对于不属于一个类的“C++”全局函数,修饰名是?test@@ZAXXZ。

这是MFC缺省调用约定。由于是调用者负责把参数弹出栈,所以可以给函数定义个数不定的参数,如printf函数。


_stdcall

按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。对于“C”函数或者变量,修饰名以下划线为前缀,然后是函数名,然后是符号“@”及参数的字节数,如函数int func(int a, double b)的修饰名是_func@12。对于“C++”函数,则有所不同。

所有的Win32 API函数都遵循该约定。


_fastcall

头两个DWORD类型或者占更少字节的参数被放入ECX和EDX寄存器,其他剩下的参数按从右到左的顺序压入栈。由被调用者把参数弹出栈,对于“C”函数或者变量,修饰名以“@”为前缀,然后是函数名,接着是符号“@”及参数的字节数,如函数int func(int a, double b)的修饰名是@func@12。对于“C++”函数,有所不同。

未来的编译器可能使用不同的寄存器来存放参数。


thiscall

仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压栈。thiscall不是关键词,因此不能被程序员指定。


naked call

采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。

naked call不是类型修饰符,故必须和_declspec共同使用,如下:

__declspec( naked ) int func( formal_parameters )

{

// Function body

}


过时的调用约定

原来的一些调用约定可以不再使用。它们被定义成调用约定_stdcall或者_cdecl。例如:

#define CALLBACK __stdcall

#define WINAPI __stdcall

#define WINAPIV __cdecl

#define APIENTRY WINAPI

#define APIPRIVATE __stdcall

#define PASCAL __stdcall


表7-1显示了一个函数在几种调用约定下的修饰名(表中的“C++”函数指的是“C++”全局函数,不是成员函数),函数原型是void CALLTYPE test(void),CALLTYPE可以是_cdecl、_fastcall、_stdcall。

表7-1 不同调用约定下的修饰名


调用约定
extern “C”或.C文件
.cpp, .cxx或/TP编译开关

_cdecl
_test
?test@@ZAXXZ

_fastcall
@test@0
?test@@YIXXZ

_stdcall
_test@0
?test@@YGXXZ


函数调用的几个概念:_stdcall,_cdecl....
(剑灵时空-www.SwordElf.com)

原作者: csdn,sinman
来自于: 不详
共有213位读者阅读过此文

内容:
左通过栈传递,被调用的函数在返回前清理传送参数的内存栈,但不同的是函数名的修饰部分(关于函数名的修饰部分在后面将详细说明)。

_stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。

2、C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。

_cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。

3、__fastcall调用约定是“人”如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。

_fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。

4、thiscall仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。

5、naked call采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。

关键字 __stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...\C/C++ \Code Generation项选择。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。

要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。

2)名字修饰约定

1、修饰名(Decoration name)

“C”或者“C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出“C++”重载函数、构造函数、析构函数,又如在汇编代码里调用“C””或“C++”函数等。

修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。

2、名字修饰约定随调用约定和编译种类(C或C++)的不同而变化。函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。

a、C编译时函数名修饰约定规则:

__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number。

__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。

__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@functionname@number。

它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。

b、C++编译时函数名修饰约定规则:

__stdcall调用约定:
1、以“?”标识函数名的开始,后跟函数名;
2、函数名后面以“@@YG”标识参数表的开始,后跟参数表;
3、参数表以代号表示:
X--void ,
D--char,
E--unsigned char,
F--short,
H--int,
I--unsigned int,
J--long,
K--unsigned long,
M--float,
N--double,
_N--bool,
....
PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复;
4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。

其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如
int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”
void Test2() -----“?Test2@@YGXXZ”

__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。

__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。

VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用.

CB在输出函数声明时使用4种修饰符号
//__cdecl
cb的默认值,它会在输出函数名前加_,并保留此函数名不变,参数按照从右到左的顺序依次传递给栈,也可以写成_cdecl和cdecl形式。
//__fastcall
她修饰的函数的参数将尽肯呢感地使用寄存器来处理,其函数名前加@,参数按照从左到右的顺序压栈;
//__pascal
它说明的函数名使用Pascal格式的命名约定。这时函数名全部大写。参数按照从左到右的顺序压栈;
//__stdcall
使用标准约定的函数名。函数名不会改变。使用__stdcall修饰时。参数按照由右到左的顺序压栈,也可以是_stdcall;

wyd124 2003-11-25
  • 打赏
  • 举报
回复
学习。。。
双杯献酒 2003-11-25
  • 打赏
  • 举报
回复
回调函数确实非常有用,
不过,它毕竟是一个“函数”,
在这个面向“对象”的编程世界里,
多少显得有些太过于底层。

以前使用回调函数的地方,
在面向对象的解决方案里,可以用COM等来代替。
这样,用一个“对象”指针,代替了“函数”指针.
yintongshun 2003-11-25
  • 打赏
  • 举报
回复
Win32程序函数调用时堆栈变化情况分析

2002-12-10 18:19:13 PCVC.NET halk 阅读次数: 4804
在经典的汇编语言教程中,函数调用时堆栈的使用都是着重讲解的问题。如今随着高级语言的越来越完善,单纯使用汇编开发的程序已经不多了。但对函数调用时堆栈动向的了解仍有助于我们明晰程序的执行流程,从而在程序编写和调试的过程中有一个清晰的思路。

一.调用约定
在Win32中,有关函数的调用主要有两种约定。
1._stdcall
以__stdcall方式调用的函数有以下特征:
• 参数由右至左压栈
• 调用返回时,堆栈由被调函数调整
2.__cdecl
__cdecl约定是C/C++函数的默认调用约定。它有以下特征:
• 参数由右至左压栈
• 调用返回时,堆栈由调用者调整

二.Win32函数调用过程

1. 压入参数
这里依据以上的调用方式将调用者给出的参数一一压入堆栈。

2. 压入断点
当程序执行到Call指令的时候,当前语句的地址作为断点地址压入堆栈。

3. 跳转
eip的值被重新设置为被调函数的起始地址。

4. mov ebp, esp
这里ebp被用来在堆栈中寻找调用者压入的参数,同时作为调用者堆栈指针的一个备份。在此前还应该执行一条:
push ebp
把ebp中原来的数值保存。

5. sub esp,N
这里N是函数内局部变量的总字节数加上一个整数,一般为40。此后esp即为被调函数的堆栈指针了。

6. 初始化esp ~ esp-N之间的N字节空间
这是对堆栈中已分配给局部变量使用的内存空间的初始化,一般全部设置为0xcc。

7. 顺序执行函数内语句。
此时函数的堆栈位于所有局部变量的内存空间之后,二者之间一般有40字节的隔离带。

8.返回
为保障调用的正常返回,函数内应当保证规范使用堆栈,使即将返回的时候esp的值恢复为执行第一条语句前的状态。说明白点,就是每一条push都要有相应的pop。
调用返回的过程如下:
mov esp, ebp
执行后,esp恢复为调用者的堆栈指针,栈顶除断点地址外,还存有原ebp的值和调用时压入的参数。
然后依次弹出ebp的值和断点地址。如果是__cdecl约定则直接返回调用者,调用者将负责调整堆栈,丢弃调先前压入的参数。如果是__stdcall则这个工作由被调函数来执行。

程序样例如下:
……
0040B8E8 push 1 ;压入参数
0040B8EA call 00401028 ;调用函数
……
00401028 jmp 0040b7c0 ;跳转到函数入口
……
0040B7C0 push ebp ;保存ebp
0040B7C1 mov ebp,esp
0040B7C3 sub esp,44h ;设置函数的堆栈指针,此函数中有4
;字节的局部变量
0040B7C6 push ebx
0040B7C7 push esi
0040B7C8 push edi
0040B7C9 lea edi,[ebp-44h]
0040B7CC mov ecx,11h
0040B7D1 mov eax,0CCCCCCCCh
0040B7D6 rep stos dword ptr [edi] ;初始化局部变量空间
0040B7D8 mov eax,dword ptr [ebp+8]
0040B7DB mov dword ptr [ebp-4],eax
……
0040B7DE pop edi ;弹出曾压栈的数据
0040B7DF pop esi
0040B7E0 pop ebx
0040B7E1 mov esp,ebp ;恢复调用者的堆栈
0040B7E3 pop ebp ;弹出原ebp值
0040B7E4 ret 4 ;返回并将堆栈向上调整4字节。
;此处为__stdcall约定,所以由函数调
;整堆栈

相应的C代码如下:

void __stdcall fun(int);

int main(void)
{
……
fun(1);
……
return 0;
}

void __stdcall fun(int para)
{
int localpara = para;
……
}



C 与C++的两种调用方法
(1)_stdcall:用于Win32 API函数和COM+接口。它从右向左将参数推入堆栈,被调函数在返回之前从堆栈中弹出自己的参数。从堆栈中弹出自己参数的函数不支持参数数目的变化。
(2)_cdecl:C和C++默认方式)。它也是从右向左传递参数。但是被调函数不负责从堆栈中弹出参数,调用函数将在函数调用返回后晴空堆栈。
CALLBACK采用方式(1)。它称为回调函数,即供系统调用的函数。
例如窗口函数、定时处理函数、线程处理函数等。

CALLBACK 是老式叫法,现在微软都改为WINAPI.



至于编译器的解释上。c与c++搞的很麻烦。

我也记不住每次碰到都去查一下.

我列个表:

语言 调用约定 输出方式

c stdcall _SomeFunctionname@Number


c cdecl _SomeFunctionname




C++的烦的要死。
C++编译时函数名修饰约定规则:

__stdcall调用约定:
1、以“?”标识函数名的开始,后跟函数名;
2、函数名后面以“@@YG”标识参数表的开始,后跟参数表;
3、参数表以代号表示:
X--void ,
D--char,
E--unsigned char,
F--short,
H--int,
I--unsigned int,
J--long,
K--unsigned long,
M--float,
N--double,
_N--bool,
....
PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复;
4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。

其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如
int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”
void Test2() -----“?Test2@@YGXXZ”

__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。

__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。


编写DLL的时候输出函数老是不认就是编译器解释的不同造成的。EXTERN "C"就是为了统一。全部用c方式解释。



两种声明是不同的,vc默认的调用是__cdecl ,所以 extern "C" __declspec(dllimport) int mydllfunc(int);是extern "C" __declspec(dllimport) int __cdecl mydllfunc(int);
因此和extern "C" __declspec(dllimport) int __stdcall mydllfunc(int);是有区别的


yintongshun 2003-11-25
  • 打赏
  • 举报
回复
贴一下:
回调函数,就是由你自己写的。你需要调用另外一个函数,而这个函数的其中一个参数,就
是你的这个回调函数名。这样,系统在必要的时候,就会调用你写的回调函数,这样你就可
以在回调函数里完成你要做的事。

capVideoStreamCallback 这个回调函数,我没有做过,看了一下Help,应该是通过发送消息
WM_CAP_SET_CALLBACK_VIDEOSTREAM,来设置的,或者调用宏capSetCallbackOnVideoStream
来完成的。这样设定之后,系统在进行图像捕捉的过程中,就会自动调用你写的回调函数。

这个回调函数的函数体需要你自已来写,然后在另一函数中调用,即是说:
LRESULT CALLBACK capVideoStreamCallback(HWND hWnd,LPVIDEOHDR lpVHdr)
{
........
}
//在另一函数中调用它(即以capVideoStreamCallback的地址作为一参数)
Function(1,......,capVideoStreamCallback,.....);
这就好像我们用定时器一样,在设置定时器时需要为定时器设置一回调函数:
::SetTimer(m_hWnd,1,1000,(TIMERPROC)TMProc);这里的TMProc就是回调函数


模块A有一个函数foo,它向模块B传递foo的地址,然后在B里面发生某种事件(event)时,通过从A里面传递过来的foo的地址调用foo,通知A发生了什么事情,让A作出相应反应。
那么我们就把foo称为回调函数。

“这个回调函数不是VFW.h中声明的么,“
----那是声明了回调函数原型,是告诉你传递进来的回调函数必须和它定义的原型保持一致。

”为什么要自己写函数体呢?“
----比如在上面模块B里面,它只知道当event发生时,向模块A发出通知,具体怎么回应这个事件,不是B所关心的,也不是B所能预料到的。
你站在A的角度上思考,当然要你自己作出对event的反应,也就是你要自己写函数体。

你如果明白了C++里面的函数指针,就很容易理解回调函数了。

"不知道系统调用后有什么结果,或者我怎么利用这个结果啊"
---如果你向系统传递一个回调函数地址,那么你的程序就相当于上面我说的模块A,系统就相当于模块B,系统只是调用你的函数,它根本不可能知道会有什么结果。
你怎么利用这个结果,看你是怎么定义这个回调函数的。
回调函数和回调机制是不同的概念,。,,函数是被调用的,但是回调机制在不同的语言中不都是以函数指针来实现的。。。。比如c#...一般的在windows api 中,会调都是使用函数指针实现的。。。



函数调用方式

我们知道在进行函数调用时,有几种调用方法,主要分为C式,Pascal式.在C和C++中
C式调用是缺省的,类的成员函数缺省调用为_stdcall。二者是有区别的,下面我们用
实例说明一下:

1. __cdecl :C和C++缺省调用方式
例子:
void Input( int &m,int &n);/*相当于void __cdecl Input(int &m,int &n);*/
以下是相应的汇编代码:
00401068 lea eax,[ebp-8] ;取[ebp-8]地址(ebp-8),存到eax
0040106B push eax ;然后压栈
0040106C lea ecx,[ebp-4] ;取[ebp-4]地址(ebp-4),存到ecx
0040106F push ecx ;然后压栈
00401070 call @ILT+5(Input) (0040100a);然后调用Input函数
00401075 add esp,8 ;恢复栈

从以上调用Input函数的过程可以看出:在调用此函数之前,首先压栈ebp-8,然
后压栈ebp-4,然后调用函数Input,最后Input函数调用结束后,利用esp+8恢复栈。由
此可见,在C语言调用中默认的函数修饰_cdecl,由主调用函数进行参数压栈并且恢复
堆栈。
下面看一下:地址ebp-8和ebp-4是什么?
在VC的VIEW下选debug windows,然后选Registers,显示寄存器变量值,然后在
选debug windows下面的Memory,输入ebp-8的值和ebp-4的值(或直接输入ebp-8和-4),
看一下这两个地址实际存储的是什么值,实际上是变量 n 的地址(ebp-8),m的地址
(ebp-4),由此可以看出:在主调用函数中进行实参的压栈并且顺序是从右到左。另外,
由于实参是相应的变量的引用,也证明实际上引用传递的是变量的地址(类似指针)。
总结:在C或C++语言调用中默认的函数修饰_cdecl,由主调用函数进行参数压栈并且
恢复堆栈,实参的压栈顺序是从右到左,最后由主调函数进行堆栈恢复。由于主调用
函数管理堆栈,所以可以实现变参函数。另外,命名修饰方法是在函数前加一个下划
线(_).

2. WINAPI (实际上就是PASCAL,CALLBACK,_stdcall)
例子:
void WINAPI Input( int &m,int &n);
看一下相应调用的汇编代码:
00401068 lea eax,[ebp-8]
0040106B push eax
0040106C lea ecx,[ebp-4]
0040106F push ecx
00401070 call @ILT+5(Input) (0040100a)

从以上调用Input函数的过程可以看出:在调用此函数之前,首先压栈ebp-8,然
后压栈ebp-4,然后调用函数Input,在调用函数Input之后,没有相应的堆栈恢复工作
(为其它的函数调用,所以我没有列出)
下面再列出Input函数本身的汇编代码:(实际此函数不大,但做汇编例子还是大了些,
大家可以只看前和后,中间代码与此例子无关)
39: void WINAPI Input( int &m,int &n)
40: {
00401110 push ebp
00401111 mov ebp,esp
00401113 sub esp,48h
00401116 push ebx
00401117 push esi
00401118 push edi
00401119 lea edi,[ebp-48h]
0040111C mov ecx,12h
00401121 mov eax,0CCCCCCCCh
00401126 rep stos dword ptr [edi]
41: int s,i;
42:
43: while(1)
00401128 mov eax,1
0040112D test eax,eax
0040112F je Input+0C1h (004011d1)
44: {
45: printf("\nPlease input the first number m:");
00401135 push offset string "\nPlease input the first number m"... (004260b8)
0040113A call printf (00401530)
0040113F add esp,4
46: scanf("%d",&m);
00401142 mov ecx,dword ptr [ebp+8]
00401145 push ecx
00401146 push offset string "%d" (004260b4)
0040114B call scanf (004015f0)
00401150 add esp,8
47:
48: if ( m= s )
004011B3 mov eax,dword ptr [ebp+8]
004011B6 mov ecx,dword ptr [eax]
004011B8 cmp ecx,dword ptr [ebp-4]
004011BB jl Input+0AFh (004011bf)
57: break;
004011BD jmp Input+0C1h (004011d1)
58: else
59: printf(" m < n*(n+1)/2,Please input again!\n");
004011BF push offset string " m < n*(n+1)/2,Please input agai"... (00426060)
004011C4 call printf (00401530)
004011C9 add esp,4
60: }
004011CC jmp Input+18h (00401128)
61:
62: }
004011D1 pop edi
004011D2 pop esi
004011D3 pop ebx
004011D4 add esp,48h
004011D7 cmp ebp,esp
004011D9 call __chkesp (004015b0)
004011DE mov esp,ebp
004011E0 pop ebp
004011E1 ret 8



最后,我们看到在函数末尾部分,有ret 8,明显是恢复堆栈,由于在32位C++中,
变量地址为4个字节(int也为4个字节),所以弹栈两个地址即8个字节。
由此可以看出:在主调用函数中负责压栈,在被调用函数中负责恢复堆栈。因此不能
实现变参函数,因为被调函数不能事先知道弹栈数量,但在主调函数中是可以做到的,
因为参数数量由主调函数确定。
下面再看一下,ebp-8和ebp-4这两个地址实际存储的是什么值,ebp-8地址存储的是n
的值,ebp -4存储的是m的值。说明也是从右到左压栈,进行参数传递。

总结:在主调用函数中负责压栈,在被调用函数中负责弹出堆栈中的参数,并且
负责恢复堆栈。因此不能实现变参函数,参数传递是从右到左。另外,命名修饰方法
是在函数前加一个下划线(_),在函数名后有符号(@),在@后面紧跟参数列表中的参数
所占字节数(10进制),如:void Input(int &m,int &n),被修饰成:_Input@8
对于大多数api函数以及窗口消息处理函数皆用 CALLBACK ,所以调用前,主调函数会
先压栈,然后api函数自己恢复堆栈。

如:
push edx
push edi
push eax
push ebx
call getdlgitemtexta

milanshi 2003-11-25
  • 打赏
  • 举报
回复
哈哈,好哦~~~~
zcchm 2003-11-25
  • 打赏
  • 举报
回复
其实回调函数不过是函数指针的应用而已,至于callback(__stdcall)是为了通用,因为大部分语言都支持这种调用方式
zcchm 2003-11-25
  • 打赏
  • 举报
回复
http://expert.csdn.net/Expert/topic/2171/2171115.xml?temp=2.062625E-02

http://expert.csdn.net/Expert/topic/2200/2200337.xml?temp=.4463617

会思考的草 2003-11-25
  • 打赏
  • 举报
回复
不过__fastcall没有压栈动作的。呵呵。刚忘了说了。
会思考的草 2003-11-25
  • 打赏
  • 举报
回复
__cdecl、__fastcall、__stdcall、__thiscall,都是压栈动作的区别。
hardstudylulin 2003-11-25
  • 打赏
  • 举报
回复
谢谢各位!又让我学到不少东西。
hardstudylulin 2003-11-24
  • 打赏
  • 举报
回复

五:万事具备

使用vc和Delphi各建立了一个工程,编写回调函数的实现部分

VC版

int WINAPI CBFunc(int Param1,int Param2)

{

int res= Param1+Param2;

TCHAR Buffer[256]="";

sprintf(Buffer,"callback result = %d",res);

MessageBox(NULL,Buffer,"Testing",MB_OK); //演示回调函数被调用

return res;

}



Delphi版

function CBFunc(Param1,Param2:integer):integer;

begin

result:= Param1+Param2;

TForm1.Edit1.Text:=inttostr(result); / /演示回调函数被调用

end;



使用静态连接的方法连接DLL里的出口函数 TestCallBack,在工程里添加 Button( 对于Delphi的工程,还需要在Form1上放一个Edit控件,默认名为Edit1)。

响应ButtonClick事件调用 TestCallBack



TestCallBack(CBFunc) //函数的参数CBFunc为回调函数的地址



函数调用创建线程后立刻返回,应用程序可以同时干别的事情去了。现在可以看到屏幕上不停的显示字符串,表示dll里创建的线程运行正常。一会之后,线程延时部分结束结束,vc的应用程序弹出MessageBox,表示回调函数被调用并显示根据Param1,Param2运算的结果,Delphi的程序edit控件里的文本则被改写成Param1,Param2 的运算结果。



可见使用回调函数的编程模式,可以根据不同的需求传递不同的回调函数地址,或者定义各种回调函数的原形(同时也需要改变使用回调函数的参数和返回值约定),实现多种回调事件处理,可以使程序的控制灵活多变,也是一种高效率的,清晰的程序模块之间的耦合方式。在一些异步或复杂的程序系统里尤其有用 -- 你可以在一个模块(如DLL)里专心实现模块核心的业务流程和技术功能,外围的扩展的功能只给出一个回调函数的接口,通过调用其他模块传递过来的回调函数地址的方式,将后续处理无缝地交给另一个模块,随它按自定义的方式处理。

 

本文的例子使用了在DLL里的多线程延时后调用回调函数的方式,只是为了突出一下回调函数的效果,其实只要是在本进程之内,都可以随你高兴可以把函数地址传递来传递去,当成回调函数使用。

 

这样的编程模式原理非常简单单一:就是把函数也看成一个指针一个地址来调用,没有什么别的复杂的东西,仅仅是编程里的一个小技巧。至于回调函数模式究竟能为你带来多少好处,就看你是否使用,如何使用这种编程模式了。
bluebohe 2003-11-24
  • 打赏
  • 举报
回复
结贴后添加精华,同时感谢:)
hardstudylulin 2003-11-24
  • 打赏
  • 举报
回复
三: 回调函数调用调用者



调用回调函数的函数我把它放到了DLL里,这是一个很简单的VC生成的WIN32 DLL.并使用DEF文件输出其函数名 TestCallBack。实现如下:



PFCALLBACK gCallBack=0;



void WINAPI TestCallBack(PFCALLBACK Func)

{

if(Func==NULL)return;

gCallBack=Func;

DWORD ThreadID=0;



HANDLE hThread = CreateThread(

NULL,

NULL,

Thread1,

LPVOID(0),

&ThreadID

);

return;

}

此函数的工作把传入的 PFCALLBACK Func参数保存起来等待使用,并且启动一个线程。声明了一个函数指针PFCALLBACK gCallBack保存传入的函数地址。





四: 回调函数如何被使用:



TestCallBack函数被调用后,启动了一个线程,作为演示,线程人为的进行了延时处理,并且把线程运行的过程打印在屏幕上.

本段线程的代码也在DLL工程里实现



ULONG WINAPI Thread1(LPVOID Param)

{



TCHAR Buffer[256];

HDC hDC = GetDC(HWND_DESKTOP);

int Step=1;

MSG Msg;

DWORD StartTick;

//一个延时循环

for(;Step<200;Step++)

{

StartTick = GetTickCount();

/*这一段为线程交出部分运行时间以让系统处理其他事务*/

for(;GetTickCount()-StartTick<10;)

{

if(PeekMessage(&Msg,NULL,0,0,PM_NOREMOVE) )

{

TranslateMessage(&Msg);

DispatchMessage(&Msg);

}

}

/*把运行情况打印到桌面,这是vcbear调试程序时最喜欢干的事情*/

sprintf(Buffer,"Running %04d",Step);

if(hDC!=NULL)

TextOut(hDC,30,50,Buffer,strlen(Buffer));

}



/*延时一段时间后调用回调函数*/

(*gCallback)(Step,1);



/*结束*/

::ReleaseDC (HWND_DESKTOP,hDC);

return 0;

}
hardstudylulin 2003-11-24
  • 打赏
  • 举报
回复
回调函数是一个很有用,也很重要的概念。当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数。回调函数在windows编程使用的场合很多,比如Hook回调函数:MouseProc,GetMsgProc以及EnumWindows,DrawState的回调函数等等,还有很多系统级的回调过程。本文不准备介绍这些函数和过程,而是谈谈实现自己的回调函数的一些经验。



之所以产生使用回调函数这个想法,是因为现在使用VC和Delphi混合编程,用VC写的一个DLL程序进行一些时间比较长的异步工作,工作完成之后,需要通知使用DLL的应用程序:某些事件已经完成,请处理事件的后续部分。开始想过使用同步对象,文件影射,消息等实现DLL函数到应用程序的通知,后来突然想到可不可以在应用程序端先写一个函数,等需要处理后续事宜的时候,在DLL里直接调用这个函数即可。



于是就动手,写了个回调函数的原形。在VC和 Delphi里都进行了测试



一:声明回调函数类型。

vc版

typedef int (WINAPI *PFCALLBACK)(int Param1,int Param2) ;



Delph版

PFCALLBACK = function(Param1:integer;Param2:integer):integer;stdcall;



实际上是声明了一个返回值为int,传入参数为两个int的指向函数的指针。

由于C++和PASCAL编译器对参数入栈和函数返回的处理有可能不一致,把函数类型用WINAPI(WINAPI宏展开就是__stdcall)或stdcall统一修饰。



二:声明回调函数原形



声明函数原形

vc版

int WINAPI CBFunc(int Param1,int Param2);



Delphi版

function CBFunc(Param1,Param2:integer):integer;stdcall;





以上函数为全局函数,如果要使用一个类里的函数作为回调函数原形,把该类函数声明为静态函数即可。

16,472

社区成员

发帖
与我相关
我的任务
社区描述
VC/MFC相关问题讨论
社区管理员
  • 基础类社区
  • Web++
  • encoderlee
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

        VC/MFC社区版块或许是CSDN最“古老”的版块了,记忆之中,与CSDN的年龄几乎差不多。随着时间的推移,MFC技术渐渐的偏离了开发主流,若干年之后的今天,当我们面对着微软的这个经典之笔,内心充满着敬意,那些曾经的记忆,可以说代表着二十年前曾经的辉煌……
        向经典致敬,或许是老一代程序员内心里面难以释怀的感受。互联网大行其道的今天,我们期待着MFC技术能够恢复其曾经的辉煌,或许这个期待会永远成为一种“梦想”,或许一切皆有可能……
        我们希望这个版块可以很好的适配Web时代,期待更好的互联网技术能够使得MFC技术框架得以重现活力,……

试试用AI创作助手写篇文章吧