一、先介绍几个背景知识
1. C/C++程序中函数参数入栈顺序默认是从右至左的。 这么设计是为了支持参数个数动态变化。先从栈中取出的,肯定是最左边的参数,这样就能够支持最右边的参数是可选的。反过来想想,如果采用自左向右的入栈方式,最前面的参数被压在栈底,这种情况下只有事先确定了参数个数,才能通过栈指针的相对位移求得最左边的参数,所以就无法支持参数个数动态变化了。
2. C/C++程序,栈是从高地址向地地址生长的,也即栈底为高地址,栈顶为低地址。结合1和2,其实我们就可以自己写个C++小程序来验证入栈顺序了。默认情况下, 右边的参数先入栈,就是高地址,左边的参数后入栈,就是低地址。
3. C/C++程序,函数调用结束后,可以由调用者负责清空栈,也可以由函数自身负责情况栈。调用者负责清空堆栈的话,因为每个调用的地方都需要生成一段调整堆栈的代码,所以最后生成的文件较大。
二、__cdecl和__stdcall的区别
__cdecl和__stdcall的区别如下表:
调用约定 | 入栈顺序 | 函数调用结束后谁负责清空堆栈 | 函数名修饰 | 说明 |
__cdecl | 从右到左 | 调用者 | 见文中描述 | C和C++程序的缺省调用方式 |
__stdcall | 从右到左 | 函数自身 | 见文中描述 |
函数名修饰
1. C语言函数名修饰:
__cdecl:在函数的前面加上下划线前缀;如 double add(double a, double b) 被修饰为 _add。
__stdcall:函数名前加上一个下划线前缀,函数名后面加上一个“@”符号和其参数的字节数;如 double add(double a, double b) 被修饰为_add@16。
2. C++ 语言函数名修饰
C++的函数名修饰规则有些复杂,但是信息更充分,通过分析修饰名不仅能够知道函数的调用方式,返回值类型,参数个数甚至参数类型。不管__cdecl,还是__stdcall调用方式,函数修饰都是以一个“?”开始,后面紧跟函数的名字,后面再跟返回类型,再后面是参数表的开始标识和按照参数类型代号拼出的参数表,参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。如 double add(double a, double b) 被修饰为 ?add@@YANNN@Z 。
X--void
D--char
E--unsigned char
F--short
H--int
I--unsigned int
J--long
K--unsigned long(DWORD)
M--float
N--double
_N--bool
U--struct
对于__stdcall方式,参数表的开始标识是“@@YG”,对于__cdecl方式则是“@@YA”。
还有当参数列表有指针的时候,指针的方式有些特别,用PA表示指针,用PB表示const类型的指针。后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复。
举一个简单的例子: int fun(int *p1, int *p2); 会被修饰为(?fun@@YAHPAH0@Z)。
还有在C++中的成员函数中公有和私有的成员函数的修饰也有相应的表示符。总而言之,在C++环境中的函数名修饰的时候,会带有参数列表的信息,还有返回值的信息,所以在C++中的函数重载就是允许存在的,因为它可以根据你的参数列表选择对应的函数,而显然在我们的C环境下是不允许的。
三、如何选择使用__cdecl和__stdcall
何时使用stdcall?
_stdcall主要用于windows API 。如果我们的函数使用了_cdecl,那么栈的清除工作是由调用者,用 COM的术语来讲就是客户来完成的。这样带来了一个棘手的问题,不同的编译器产生栈的方式不尽相同,那么调用者能否正常的完成清除工作呢?答案是不能。如果使用__stdcall,上面的问题就解决了,函数自己解决清除工作。所以,在跨(开发)平台的调用中,我们都使用__stdcall(虽然有时是以 WINAPI的样子出现)。
何时使用cdecl?
_cdecl对于变长参数适用。当我们遇到这样的函数如fprintf()它的参数是可变的,不定长的,被调用者事先无法知 道参数的长度,事后的清除工作也无法正常的进行,因此,这种情况我们只能使用_cdecl。
四、extern "C"
前面讲到了不论是cdecl还是stdcall,C和C++对函数名修饰都是不同的规则。那么在C和C++之前互相引用的时候,就要使用extern C了。
情况1::在C++中包含C语言写的头文件的时候,将C语言头文件包含在extern "C" 中,这样C++才能调用C声明和定义的函数。如下是一个常见的代码结构,
1 #ifndef __INCvxWorksh /*防止该头文件被重复引用*/
2 #define __INCvxWorksh
3 #ifdef __cplusplus
4 extern "C"{ //告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
5 #endif
6
7 /*…*/
8
9 #ifdef __cplusplus
10 }
11
12 #endif
13 #endif /*end of __INCvxWorksh*/
情况2:C中引用C++语言中的函数或者变量时,C++的头文件需要加上extern “C”,但是C语言中不能直接引用声明了extern “C”的该头文件,应该仅在C中将C++中定义的extern “C”函数声明为extern类型。
1 /* c++头文件cppExample.h */
2 #ifndef CPP_EXAMPLE_H
3 #define CPP_EXAMPLE_H
4 extern "C" int add(int x, int y);
5 #endif
6
7 /* c实现文件cFile.c */
8 extern int add(int x, int y);
9 int main()
10 {
11 add(2, 3);
12 return 0;
13 }