C/C++:函数的编译方式与调用约定以及extern “C”的使用

函数在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调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为[email protected]。

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

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

它们均不改变输出函数名中的字符大小写,这和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”标识结束。

其格式为“[email protected]@yg*****@z”或“[email protected]@yg*xz”,例如

int test1-----“[email protected]@[email protected]”

void test2-----“[email protected]@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程序默认使用的调用约定,也可以在函数声明时加上__cdecl关键字来手工指定。采用__cdecl约定时,函数参数按

照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能使用该调用约定。由于每一个使用

__cdecl约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大。__cdecl可以写成_cdecl。

2、__stdcall

__stdcall调用约定用于调用Win32 API函数。采用__stdcal约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参

数的栈,函数参数个数固定。由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条ret n指令直接清理传递参数的堆

栈。__stdcall可以写成_stdcall。

3、__fastcall

__fastcall约定用于对性能要求非常高的场合。__fastcall约定将函数的从左边开始的两个大小不大于4个字节(DWORD)的参数分别放在

ECX和EDX寄存器,其余的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的堆栈。__fastcall可以写成_fastcall。

关键字__cdecl、__stdcall和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...->C/C++->Code Generation项选

择。它们对应的命令行参数分别为/Gd、/Gz和/Gr。缺省状态为/Gd,即__cdecl。当加在输出函数前的关键字与编译环境中的选择不同时,直

接加在输出函数前的关键字有效。

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 __cplusplus

  extern "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函数例子工程中包含的三个文件的源代码如下:

  /* c语言头文件:cExample.h */

  #ifndef C_EXAMPLE_H

  #define C_EXAMPLE_H

  extern 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” 关键字),应该在C中将需要引用的C++

函数的声明为extern类型。

以C引用C++函数例子工程中包含的三个文件的源代码如下:

  //C++头文件 cppExample.h

  #ifndef CPP_EXAMPLE_H

  #define CPP_EXAMPLE_H

  extern "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" 关键字 */

  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).

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-12-28 02:47:44

C/C++:函数的编译方式与调用约定以及extern “C”的使用的相关文章

C++调用约定和名字约定

调用约定是指程序在函数调用时传递参数和获取返回值所采用的方法:通过寄存器.或通过栈.或者是两者的混合.用于指定Calling Convention的修饰符主要有:__cdecl,__stdcall,__fastcall等.调用约定可以通过工程设置:Setting...\C/C++ \Advanced\CallingConvention 项进行选择,缺省状态为__cdecl.它们决定以下内容: 1).函数参数的压栈顺序, 2).由调用者还是被调用者把参数弹出栈, 3).以及产生函数修饰名的方法.

[Lua]Lua调用C/C++函数/库(函数压栈方式)

test.cpp文件 /*Lua调用C/C++函数/库(函数压栈方式)*/ #include<iostream> using namespace std; #include<lua.hpp> /* 当我们需要在Lua里面调用C/C++函数时,所有的函数都必须满足以下函数签名: typedef int (*lua_CFunction) (lua_State *L);换句话说,所有的函数必须接收一个lua_State作为参数,同时返回一个整数值.因为这个函数使用Lua栈作为参数,所以它

虚函数 动态绑定 实现方式是:虚函数表

定义为 virtual的 函数是基类期待派生类(derived class)重定义的, 基类期待 派生类继承的函数不能定义为虚函数. 动态绑定:程序能 使用继承层次中任意类型的对象,无需关心对象的具体类型.  使用这些类的程序无须区分函数在基类还是在继承类中定义的. 尽管不是必须,派生类一般会重定义基类中的虚函数.如果没有重定义,则继承基类中定义的版本.派生类中虚函数的声明原型必须与基类中的定义方式完全匹配,但有一个例外:基类中返回值是对基类性的引用(或指针)的虚函数,在派生类中虚函数可以返回

[转]Debug 和 Release 编译方式的区别

本文主要包含如下内容: 1. Debug 和 Release 编译方式的本质区别2. 哪些情况下 Release 版会出错3. 怎样"调试" Release 版的程序Debug 和 Release 编译方式的本质区别    Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序.Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用.    Debug 和 Release 的真正秘密,在于一组编译选

【原创+整理】简述何为调用约定,函数导出名以及extern C

何为调用约定 调用约定指的是函数在调用时会按照不同规则,翻译成不同的汇编代码.这和参数的压栈顺序和栈的清理方式相关,也就是说不同的调用约定,这些方式会做相应改变.一般编译器是以默认的调用约定编译一份代码,但当一个项目使用不同调用约定的库会产生链接错误. 何为函数导出名 同一个函数,在不同的编译器编译出来的符号名是不一样的,程序目标文件链接的时候不知道源程序的函数名,而是通过目标文件(.obj)中寻找相应的函数符号表.在下面中会指出不同调用约定对应的函数导出名. 三种调用约定   (1)__fas

MFC DLL 导出函数的定义方式

一直在鼓捣DLL,每天的工作都是调试一个一个的DLL,往DLL里面添加自己的代码,但是对于DLL一直不太了解啊!今天一查资料,才发现自己对于DLL编写的一些基本知识也不了解.要学习,这篇文章先总结DLL的导出函数的方法. 1. 首先说一下如何建立一个普通的DLL工程!(以VS2008为例) New Project  -->  Win32 标签 --> 填写工程名称 -->  点 OK,进入创建 Widzard  -->  Next 进入第二步 -->  Application

c++成员函数的存储方式---11

原创博客:转载请标明出处:http://www.cnblogs.com/zxouxuewei/ 成员函数属于一个类的成员,出现再类体中.可以被指定为公有,私有或受保护的. 1.在类外面定义成员函数时,需要用类名加作用域限定符(::),如下代码: #include <iostream> #include <string> using namespace std; class _test_myclass { public: void display(const int &val

VS中的Debug 和 Release 编译方式的本质区别

VS中的Debug 和 Release 编译方式的本质区别 Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序.Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用. Debug 和 Release 的真正秘密,在于一组编译选项.下面列出了分别针对二者的选项(当然除此之外还有其他一些,如/Fd /Fo,但区别并不重要,通常他们也不会引起 Release 版错误,在此不讨论) 以下参数均是在project

C++成员函数的存储方式

用类去定义对象时,系统会为每一个对象分配存储空间.如果一个类包括了数据和函数,要分别为数据和函数的代码分配存储空间. 按理说,如果用同一个类定义了10个对象,那么就需要分别为10个对象的数据和函数代码分配存储单元,如图8.4所示. 能否只用一段空间来存放这个共同的函数代码段,在调用各对象的函数时,都去调用这个公用的函数代码.如图8.5所示. 显然,这样做会大大节约存储空间.C++编译系统正是这样做的,因此每个对象所占用的存储空间只是该对象的数据部分所占用的存储空间,而不包括函数代码所占用的存储空