_cdecl 与 _stdcal 区别 以及 extern “c‘ 作用

目的

1) 了解windows 下 _cdecl 与 _stdcal区别, 方便更好运用修饰函数进行函数调用时,有几种调用方法,分为C式,Pascal式。总的区别来说。

  • _cdecl(c/c++缺省的,支持变参函数,但其他非c/c++语言不支持)
  • _stdcall(依词义,标准调用,不支持变参)

2) dll extern "c‘ 作用

_stdcal

WINAPI (实际上就是PASCAL,CALLBACK,_stdcall)

  void   WINAPI   Input(   int   &m,int   &n);  

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

_cdecl

在C和C++中C式调用是缺省的,除非特殊声明。

  void   Input(   int   &m,int   &n);  

特点: 在C或C++语言调用中默认的函数修饰_cdecl,由主调用函数进行参数压栈并且恢复堆栈,实参的压栈顺序是从右到左,最后由主调函数进行堆栈恢复。由于主调用函数管理堆栈,所以可以实现变参函数。另外,命名修饰方法是在函数前加一个下划线(_).

证明实际上引用传递的是变量的地址(类似指针)

结果

__stdcall 与 _cdecl

The __stdcall calling convention is used to call Win32 API functions.
 The callee cleans the stack, so the compiler makes vararg functions __cdecl. Functions that use this calling convention require a function prototype. The __stdcall modifier is Microsoft-specific.

__stdcall 调用约定用于调用 Win32 API 函数。被调用者清理堆栈,因此编译器将可变参数函数设为 __cdecl。使用此调用约定的函数需要函数原型。 __stdcall 修饰符是 Microsoft 特定的。

_cdecl 与 _stdcall 详解

__stdcall

dll extern “c”

动态链接库的使用有两种方式,一种是显式调用。一种是隐式调用。

  • 显式调用:使用LoadLibrary载入动态链接库、使用GetProcAddress获取某函数地址。
  • 隐式调用:可以使用#pragma comment(lib, “XX.lib”)的方式,也可以直接将XX.lib加入到工程中。

dll编写需要解决函数重命名——Name-Mangling。解决方式有两种,一种是直接在代码里解决采用extent”c”、_declspec(dllexport)、#pragma comment(linker, “/export:[Exports Name]=[Mangling Name]”),另一种是采用def文件。

因为C和C++的重命名规则是不一样的。这种重命名称为“Name-Mangling”(名字修饰或名字改编、标识符重命名)。

据说,C++标准并没有规定Name-Mangling的方案,所以不同编译器使用的是不同的,例如:Borland C++跟Mircrosoft C++就不同,而且可能不同版本的编译器他们的Name-Mangling规则也是不同的。这样的话,不同编译器编译出来的目标文件.obj 是不通用的,因为同一个函数,使用不同的Name-Mangling在obj文件中就会有不同的名字。如果DLL里的函数重命名规则跟DLL的使用者采用的重命名规则不一致,那就会找不到这个函数。

C标准规定了C语言Name-Mangling的规范(林锐的书有这样说过)。这样就使得,任何一个支持C语言的编译器,它编译出来的obj文件可以共享,链接成可执行文件。这是一种标准,如果DLL跟其使用者都采用这种约定,那么就可以解决函数重命名规则不一致导致的错误。

影响符号名的除了C++和C的区别、编译器的区别之外,还要考虑调用约定导致的Name Mangling。如extern “c” __stdcall的调用方式就会在原来函数名上加上写表示参数的符号,而extern “c” __cdecl则不会附加额外的符号。

dll中的函数在被调用时是以函数名或函数编号的方式被索引的。这就意味着采用某编译器的C++的Name-Mangling方式产生的dll文件可能不通用。因为它们的函数名重命名方式不同。为了使得dll可以通用些,很多时候都要使用C的Name-Mangling方式,即是对每一个导出函数声明为extern “C”,而且采用_stdcall调用约定,接着还需要对导出函数进行重命名,以便导出不加修饰的函数名。

注意到extern “C”的作用是为了解决函数符号名的问题,这对于动态链接库的制造者和动态链接库的使用者都需要遵守的规则。

动态链接库的显式装入就是通过GetProcAddress函数,依据动态链接库句柄和函数名,获取函数地址。因为GetProcAddress仅是操作系统相关,可能会操作各种各样的编译器产生的dll,它的参数里的函数名是原原本本的函数名,没有任何修饰,所以一般情况下需要确保dll’里的函数名是原始的函数名。分两步:一,如果导出函数使用了extern”C” _cdecl,那么就不需要再重命名了,这个时候dll里的名字就是原始名字;如果使用了extern”C” _stdcall,这时候dll中的函数名被修饰了,就需要重命名。二、重命名的方式有两种,要么使用*.def文件,在文件外修正,要么使用#pragma,在代码里给函数别名。

  • _declspec(dllexport)和_declspec(dllimport)的作用

    _declspec还有另外的用途,这里只讨论跟dll相关的使用。正如括号里的关键字一样,导出和导入。_declspec(dllexport)用在dll上,用于说明这是导出的函数。而_declspec(dllimport)用在调用dll的程序中,用于说明这是从dll中导入的函数。

     因为dll中必须说明函数要用于导出,所以_declspec(dllexport)很有必要。但是可以换一种方式,可以使用def文件来说明哪些函数用于导出,同时def文件里边还有函数的编号。
    

而使用_declspec(dllimport)却不是必须的,但是建议这么做。因为如果不用_declspec(dllimport)来说明该函数是从dll导入的,那么编译器就不知道这个函数到底在哪里,生成的exe里会有一个call XX的指令,这个XX是一个常数地址,XX地址处是一个jmp dword ptr[XXXX]的指令,跳转到该函数的函数体处,显然这样就无缘无故多了一次中间的跳转。如果使用了_declspec(dllimport)来说明,那么就直接产生call dword ptr[XXX],这样就不会有多余的跳转了

  • __stdcall影响

这是一种函数的调用方式。默认情况下VC使用的是__cdecl的函数调用方式,如果产生的dll只会给C/C++程序使用,那么就没必要定义为__stdcall调用方式,如果要给Win32汇编使用(或者其他的__stdcall调用方式的程序),那么就可以使用__stdcall。这个可能不是很重要,因为可以自己在调用函数的时候设置函数调用的规则。像VC就可以设置函数的调用方式,所以可以方便的使用win32汇编产生的dll。不过__stdcall这调用约定会Name-Mangling,所以我觉得用VC默认的调用约定简便些。但是,如果既要__stdcall调用约定,又要函数名不给修饰,那可以使用*.def文件,或者在代码里#pragma的方式给函数提供别名(这种方式需要知道修饰后的函数名是什么)。

·extern “C” __declspec(dllexport) bool  __stdcall cswuyg();
·extern “C”__declspec(dllimport) bool __stdcall cswuyg();
·#pragma comment(linker, "/export:cswuyg=_cswuyg@0")

结论:
总的来说,在编写DLL的时候,写个头文件,头文件里声明函数的NameMingling方式、调用约定(主要是为了隐式调用)。再写个
.def文件把函数重命名了(主要是为了显式调用)。提供
.DLL*.lib*.h给dll的使用者,这样无论是隐式的调用,还是显式的调用,都可以方便的进行。**

很感谢这位博客主写的DLL编写中extern “C”和__stdcall的作用 他写的详细和很清楚了。

补充

1)调用协议常用场合

  • __stdcall:Windows API默认的函数调用协议。
  • __cdecl:C/C++默认的函数调用协议。
  • __fastcall:适用于对性能要求较高的场合。

2)函数参数入栈方式

  • __stdcall:函数参数由右向左入栈。
  • __cdecl:函数参数由右向左入栈。
  • __fastcall:从左开始不大于4字节的参数放入CPU的ECX和EDX寄存器,其余参数从右向左入栈。
    问题一:__fastcall在寄存器中放入不大于4字节的参数,故性能较高,适用于需要高性能的场合。

3)栈内数据清除方式

  • __stdcall:函数调用结束后由被调用函数清除栈内数据。
  • __cdecl:函数调用结束后由函数调用者清除栈内数据。
  • __fastcall:函数调用结束后由被调用函数清除栈内数据。
    问题一:不同编译器设定的栈结构不尽相同,跨开发平台时由函数调用者清除栈内数据不可行。
    问题二:某些函数的参数是可变的,如printf函数,这样的函数只能由函数调用者清除栈内数据。
    问题三:由调用者清除栈内数据时,每次调用都包含清除栈内数据的代码,故可执行文件较大。

4)C语言编译器函数名称修饰规则

  • __stdcall:编译后,函数名被修饰为“_functionname@number”。
  • __cdecl:编译后,函数名被修饰为“_functionname”。
  • __fastcall:编译后,函数名给修饰为“@functionname@nmuber”。
    注:“functionname”为函数名,“number”为参数字节数。
    注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。

5)C++语言编译器函数名称修饰规则

  • __stdcall:编译后,函数名被修饰为“?functionname@@YG******@Z”。
  • __cdecl:编译后,函数名被修饰为“?functionname@@YA******@Z”。
  • __fastcall:编译后,函数名被修饰为“?functionname@@YI******@Z”。
    注:“******”为函数返回值类型和参数类型表。
    注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。

6)C语言和C++语言间如果不进行特殊处理,也无法实现函数的互相调用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

道阻且长,行则降至

无聊,打赏求刺激而已

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值