C函数调用约定

C中采用了不同的调用方式来调用函数,这里的函数调用概念可能与我们通常所理解的函数调用有所不同,它们指的是处理器在处理函数上的差异。理解这些不同的方式有助于我们来调试程序和链接我们的代码。在此我想讨论一下主要的四种函数调用方法以及之间的区别,它们是__stdcall__cdecl__fastcallthiscall。当然,还有一些更加不常用的函数调用方法比如naked call我也将顺便提及。

   
不同的函数调用方法之间的主要有以下一些区别:
当参数个数多于一个时,按照什么顺序把参数压入堆栈函数调用后
谁来恢复堆栈
编译后的修饰名规则

__stdcall
:将参数压栈是按PASCAL语言的顺序(从右到左),通常用于WINAPI中。它是由被调用者将参数从栈中清除的,所以它的编译文件比__cdecl小。__stdcallWindows API函数中默认的调用约定,被调函数自己在退出时清空堆栈。这种调用方式不能实现变参函数,因为被调函数不能事先知道弹栈数量,但在主调函数中是可以做到的,因为参数数量由主调函数确定。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。如函数int func(int a,  double b)的修饰名是_func@12
编译选项/Gz


注意:在创建DLL时,一般使用__stdcall调用(Win32 Api方式),采用__functionname@number命名规则,因而各种语言间的DLL能互相调用。也就是说,DLL的编制与具体的编程语言及编译器无关,只要遵守DLL的开发规范和编程策略,并安排正确的调用接口,不管用何种编程语言编制的DLL都具有通用性。


__cdecl
:是C语言采用的默认调用方法,参数按从右到左的顺序压入栈,由调用者把参数弹出栈,它的优点是支持printf这样的可变参数调用。一般可变参数函数的调用都采用这种方式,比如int __cdecl scanf (const char *format,…)。对于"C",修饰名是在函数名前加下划线,如函数void test(void)的修饰名是__test。除非声明为 extern "C",否则 C++ 函数将使用不同的名称修饰方案。
   
编译选项/Gd

   
注意:CC++中的main(wmain)函数的调用约定必须是__cdecl,不允许更改。


__fastcall
__fastcall调用较快,它通过CPU内部寄存器传递参数。头两个DWORD类型或者占更少字节的参数被放入ECXEDX寄存器,其他剩下的参数按从右到左的顺序压入栈。由被调用者把参数弹出栈,对于“C”函数或者变量,修饰名以“@”为前缀,然后是函数名,接着是符号“@”及参数的字节数,如函数int func(int  a, double b)的修饰名是@func@12
编译选项/Gr,通常减少执行时间。


   
注意:在对用内联程序集语言编写的任意函数使用__fastcall 调用约定时,一定要小心。您对寄存器的使用可能与编译器对它们的使用发生冲突。Microsoft 不保证不同编译器版本之间的 __fastcall 调用约定的实现相同。例如,16 位编译器与 32 位编译器的实现就不同。因此当使用 __fastcall 命名约定时,请使用标准包含文件。否则将获取无法解析的外部引用。


   thiscall
函数体 this指针默认通过ECX传递,其它参数从右到左入栈。thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理。
thiscall
意味着:参数从右向左入栈,如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈。对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈。

 

有人对用用的编译器的调用约定进行了小部分总结,share一下。

VC6:


        调用约定        堆栈清除    参数传递
        __cdecl                  调用者      从右到左,通过堆栈传递
        __stdcall                  函数体         从右到左,通过堆栈传递
        __fastcall                 函数体         从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈
        thiscall                     函数体         this指针默认通过ECX传递,其他参数从右到左入栈


__cdecl是C\C++的默认调用约定; VC的调用约定中并没有thiscall这个关键字,他是类成员函数默认调用约定;
C\C++中的main(或wmain)函数的调用约定必须是__cdecl,不允许更改;
默认调用约定一般能够通过编译器配置进行更改,假如您的代码依赖于调用约定,请明确指出需要使用的调用约定;

C++Builder6:


        调用约定        堆栈清除    参数传递
        __fastcall                  函数体      从左到右,优先使用寄存器(EAX,EDX,ECX),然后使用堆栈 (兼容Delphi的register)
        (register和__fastcall等同)
        __pascal                     函数体      从左到右,通过堆栈传递
        __cdecl                    调用者    从右到左,通过堆栈传递(和C\C++默认调用约定兼容)
        __stdcall                     函数体      从右到左,通过堆栈传递(和VC中的__stdcall兼容)
        __msfastcall                函数体      从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈(兼容VC的__fastcall)

 


 

总结:调用约定是由函数类型来决定的,而又是和编译器相关的。代码不具备移植性。但是,函数参数入栈顺序一般都是从右到左的,这个要mark一下。

 

来一段程序显示一下函数参数的地址和调用参数的地址:

#include int f(int i,int j,int k); int main() { static int i=0; printf("i=%d:[%x]/n",i,&i); f(i++,i++,i++); int j=0; printf("/n/nj=%d:[%x]/n",j,&j); f(j++,j++,j++); return 0; } int f(int i,int j,int k) { int l; int g; printf("i=%d:[%x]/n",i,&i); printf("j=%d:[%x]/n",j,&j); printf("k=%d:[%x]/n",k,&k); printf("___________/n"); printf("l:%x/n",&l); printf("g:%x/n",&g); printf("function f() end___________/n/n"); }

 

执行结果:

i=0:[8049768]
i=2:[bfffe630]
j=1:[bfffe634]
k=0:[bfffe638]
___________
l:bfffe624
g:bfffe620
function f() end___________

 

j=0:[bfffe654]
i=2:[bfffe630]
j=1:[bfffe634]
k=0:[bfffe638]
___________
l:bfffe624
g:bfffe620
function f() end___________

你可能感兴趣的:(C/C++)