函数在C++编译方式与C编译方式下的主要不同在于:因为C++引入了函数重载(overload),因此编译器对同名函数进行了名称重整(name mangle)。因此。在C++中引
用其它C函数库时,须要对声明使用的函数做适当的处理。以告知编译器做出适应的名称处理。
函数的调用约定涉及了函数參数的入栈顺序、清栈主体(负责清理栈的主体:函数自身还是调用函数者?)、部分名称重整。
如,在C编译方式下有_stdcall、_cdecl等调用约定,在C++编译方式下也有_stdcall、_cedecl等调用约定。
两个复杂修饰的样例:
extern "C" _declspec(dllexport) int __cdecl Add(int a, int b); //C编译方式导出_cdecl调用约定函数
typedef int (__cdecl*FunPointer)(int a, int b);
转自:http://blog.csdn.net/H2SO2H2SO2/article/details/4207127
1.编译方式
c编译时函数名修饰约定规则:
__stdcall调用约定在输出函数名前加上一个下划线前缀。后面加上一个“@”符号和其參数的字节数,格式为_functionname@number。
__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。
__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其參数的字节数,格式@functionname@number。
它们均不改变输出函数名中的字符大写和小写,这和pascal调用约定不同。pascal约定输出的函数名无不论什么修饰且所有大写。
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-----“?
test1@@yghpadk@z”
void test2-----“?test2@@ygxxz”
__cdecl调用约定:
规则同上面的_stdcall调用约定。仅仅是參数表的開始标识由上面的“@@yg”变为“@@ya”。
__fastcall调用约定:
规则同上面的_stdcall调用约定,仅仅是參数表的開始标识由上面的“@@yg”变为“@@yi”。
2.调用约定
调用约定(Calling Convention)是指在程序设计语言中为了实现函数调用而建立的一种协议。这样的协议规定了该语言的函数中的參数传送方
式、參数是否可变和由谁来处理堆栈等问题。不同的语言定义了不同的调用约定。
在C++中,为了同意操作符重载和函数重载。C++编译器往往依照某种规则改写每个入口点的符号名,以便同意同一个名字(具有不同的參
数类型或者是不同的作用域)有多个使用方法。而不会打破现有的基于C的链接器。这项技术通常被称为名称改编(Name Mangling)或者名称修
饰(Name Decoration)。很多C++编译器厂商选择了自己的名称修饰方案。
因此,为了使其他语言编写的模块(如Visual Basic应用程序、Pascal或Fortran的应用程序等)能够调用C/C++编写的DLL的函数,必须使
用正确的调用约定来导出函数,而且不要让编译器对要导出的函数进行不论什么名称修饰。
调用约定用来:(一)处理决定函数參数传送时入栈和(二)出栈的顺序(由调用者还是被调用者把參数弹出栈),以及(三)编译器用来识别函数名
称的名称修饰约定等问题。
1、__cdecl
__cdecl是C/C++和MFC程序默认使用的调用约定。也能够在函数声明时加上__cdeclkeyword来手工指定。採用__cdecl约定时。函数參数按
照从右到左的顺序入栈,而且由调用函数者把參数弹出栈以清理堆栈。
因此。实现可变參数的函数仅仅能使用该调用约定。
因为每个使用
__cdecl约定的函数都要包括清理堆栈的代码,所以产生的可运行文件大小会比較大。__cdecl能够写成_cdecl。
2、__stdcall
__stdcall调用约定用于调用Win32 API函数。採用__stdcal约定时,函数參数依照从右到左的顺序入栈。被调用的函数在返回前清理传送參
数的栈,函数參数个数固定。
因为函数体本身知道传进来的參数个数,因此被调用的函数能够在返回前用一条ret n指令直接清理传递參数的堆
栈。
__stdcall能够写成_stdcall。
3、__fastcall
__fastcall约定用于对性能要求很高的场合。__fastcall约定将函数的从左边開始的两个大小不大于4个字节(DWORD)的參数分别放在
ECX和EDX寄存器。其余的參数仍旧自右向左压栈传送,被调用的函数在返回前清理传送參数的堆栈。__fastcall能够写成_fastcall。
keyword__cdecl、__stdcall和__fastcall能够直接加在要输出的函数前。也能够在编译环境的Setting...->C/C++->Code Generation项选
择。它们相应的命令行參数分别为/Gd、/Gz和/Gr。缺省状态为/Gd,即__cdecl。当加在输出函数前的keyword与编译环境中的选择不同一时候,直
接加在输出函数前的keyword有效。
3._stdcall与_cdecl调用约定对照
在“windef.h”头文件里可找到:
#define CALLBACK __stdcall#define WINAPI __stdcall#define WINAPIV __cdecl#define APIENTRY WINAPI#define APIPRIVATE __stdcall#define PASCAL __stdcall#define cdecl _cdecl#ifndef CDECL#define CDECL _cdecl#endif
差点儿我们写的每个WINDOWS API函数都是__stdcall类型的,为什么?
首先,我们谈一下两者之间的差别:WINDOWS的函数调用时须要用到栈(STACK,一种先入后出的存储结构)。
当函数调用
完毕后。栈须要清除,这里就是问题的关键,怎样清除?假设我们的函数使用了__cdecl。那么栈的清除工作是由调用者,用
COM的术语来讲就是客户来完毕的。这样带来了一个棘手的问题,不同的编译器产生栈的方式不尽同样,那么调用者是否能正常
的完毕清除工作呢?答案是不能。
假设使用__stdcall,上面的问题就攻克了,函数自己解决清除工作。所以。在跨(开发)平
台的调用中。我们都使用__stdcall(尽管有时是以WINAPI的样子出现)。那么为什么还须要_cdecl呢?当我们遇到这种函
数如fprintf()它的參数是可变的,不定长的,被调用者事先无法知道參数的长度,事后的清除工作也无法正常的进行。因此,这
种情况我们仅仅能使用_cdecl。
注意:
1、_beginthread须要__cdecl的线程函数地址。_beginthreadex和CreateThread须要__stdcall的线程函数地址。
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);
4、extern ”C” 的作用(參考:http://hi.baidu.com/qinfengxiaoyue/item/8bd89e81d1cbeb5226ebd9b4)
为什么标准头文件都有类似下面的结构?
#ifndef __INCvxWorksh#define __INCvxWorksh#ifdef __cplusplusextern "C" {#endif/*...*/
#ifdef __cplusplus}#endif#endif /* __INCvxWorksh */
显然,头文件里的编译宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止该头文件被反复引用。
那么
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
的作用又是什么呢?
答:被extern "C" 修饰的变量和函数是依照C语言方式编译和连接的;即为实现C++与C语言的混合编程。
明确了C++中extern "C"的设立动机,我们以下来详细分析extern "C"通常的使用技巧。
extern "C"的惯使用方法:
(1)在C++中引用C语言中的函数和变量,在包括C语言头文件(如果为cExample.h)时。需进行下列处理:
extern "C"
{
#include "cExample.h"
}
而在C语言的头文件里。对其外部函数仅仅能指定为extern类型,C语言中不支持extern "C"声明。在.c文件里包括了extern "C"时会出现编
译语法错误。
以C++引用C函数样例project中包括的三个文件的源码例如以下:
/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H#define C_EXAMPLE_Hextern int add(int x,int y);#endif
/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y ){return x + y;
}
// c++实现文件。调用add:cppFile.cpp
extern "C"{#include "cExample.h"
}int main(int argc, char* argv[]){add(2,3);return 0;
}
假设C++调用一个C语言编写的.DLL时,当包含.DLL的头文件或声明接口函数时。应加extern "C" { }。
(2)在C中引用C++语言中的函数和变量时,C++的头文件里的函数声明需加入前缀extern "C",可是在C语言中不能直接引用
已由extern "C"修饰过的函数声明或变量的头文件(由于C编译方式不支持extern “C” keyword),应该在C中将须要引用的C++
中函数的声明为extern类型。
以C引用C++函数样例project中包括的三个文件的源码例如以下:
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H#define CPP_EXAMPLE_Hextern "C" int add( int x, int y );#endif
//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y ){return x + y;
}
/* C实现文件 cFile.c
/* 但这样会编译出错:#include "cExample.h",由于C编译不支持extern "C" keyword */extern int add( int x, int y );int main( int argc, char* argv[] ){add( 2, 3 );return 0;
}
5、MFC提供了一些宏,能够使用AFX_EXT_CLASS来取代__declspec(DLLexport)。并修饰类名,从而导出类。
AFX_API_EXPORT来修饰函数,AFX_DATA_EXPORT来修饰变量
AFX_CLASS_IMPORT:__declspec(DLLexport)
AFX_API_IMPORT:__declspec(DLLexport)
AFX_DATA_IMPORT:__declspec(DLLexport)
AFX_CLASS_EXPORT:__declspec(DLLexport)
AFX_API_EXPORT:__declspec(DLLexport)
AFX_DATA_EXPORT:__declspec(DLLexport)
AFX_EXT_CLASS:#ifdef _AFXEXT
AFX_CLASS_EXPORT
#else
AFX_CLASS_IMPORT
6、DLLMain负责初始化(Initialization)和结束(Termination)工作,每当一个新的进程或者该进程的新的线程訪问DLL时,或
者訪问DLL的每个进程或者线程不再使用DLL或者结束时,都会调用DLLMain。可是。使用TerminateProcess或
TerminateThread结束进程或者线程。不会调用DLLMain。
7、一个DLL在内存中仅仅有一个实例
DLL程序和调用其输出函数的程序的关系:
1)、DLL与进程、线程之间的关系
DLL模块被映射到调用它的进程的虚拟地址空间。
DLL使用的内存从调用进程的虚拟地址空间分配。仅仅能被该进程的线程所訪问。
DLL的句柄能够被调用进程使用;调用进程的句柄能够被DLL使用。
DLL能够有自己的数据段,但没有自己的堆栈。使用调用进程的栈。与调用它的应用程序同样的堆栈模式。
2)、关于共享数据段
DLL定义的全局变量能够被调用进程訪问;DLL能够訪问调用进程的全局数据。
使用同一DLL的每个进程都有自己的DLL全局
变量实例。假设多个线程并发訪问同一变量。则须要使用同步机制。对一个DLL的变量,假设希望每一个使用DLL的线程都有自己
的值,则应该使用线程局部存储(TLS,Thread Local Strorage).