函数的调用约定其实比较简单,并不复杂,但很多人对这一块内容不太了解,甚至连工作几年的朋友也不太清楚。最近有朋友想了解这一块的内容,所以今天我们就来讲一下C/C++函数调用约定相关的内容。
1、概述
常见的函数调用约定有__cdecl C调用、__stdcall标准调用、__fastcall快速调用以及__pascal调用:
这些调用是开发语言中的关键字,放置在函数前,用来指定函数的调用约定,比如:
BOOL __stdcall InitSDK();
如上所示,调用约定关键字一般放于返回值类型与函数之间。而函数返回值类型前面一般放置函数的导入导出声明:(dll库的函数接口有导入导出之分)
// 定义导出导入SDK_DLL_API宏 #ifdef DLL_EXPORTS #define SDK_DLL_API _declspec(dllexport) #else #define SDK_DLL_API _declspec(dllimport) #endif // 将导出导入SDK_DLL_API宏放到返回值类型之前 SDK_DLL_API BOOL __stdcall InitSDK();
函数的调用约定主要决定三方面的内容:
1)函数参数的入栈顺序
函数调用时主调函数的参数是通过栈传递给被调用函数的。从汇编上看的比较清晰,在call函数之前,会将参数的值压到栈上,比如:
如果函数有多个参数,则会有两种入栈方式,一种是从右到左依次入栈,一种是从左到右依次入栈,这是函数调用约定决定的。
2)参数栈空间由谁来释放
函数调用完成后传递给被调用函数的参数的占用的栈空间是需要释放掉的,专业术语叫“平栈”,清理掉参数的栈空间才能做到栈平衡。参数占用的栈空间到底是谁来清理,也是函数调用约定决定的。编译器在编译链接生成汇编代码时,就生成好了清理参数栈空间的汇编代码。
3)编译时的函数名称改编
不同的调用约定下编译生成的函数名称格式可能是不同的。C++之所以支持函数重载(源代码中,函数名称相同,函数参数不同),就是因为C++编译器会对函数名称进行改编,改编后的名称中包含参数类型进而能区分出重载的函数。
2、常见的调用约定说明
常见的函数调用约定有__cdecl C调用、__stdcall标准调用、__fastcall快速调用以及__pascal调用。C/C++ 中主要使用__cdecl C调用、__stdcall标准调用、__fastcall快速调用三种。__pascal 是用于 Pascal / Delphi 编程语言的调用规则,C/C++ 中也可以使用这种调用规则,但该调用约定已经被C++废弃,不提倡使用了。
下面我们来看看这几种调用约定的异同点,见下面的表格:
2.1、__cdecl C调用
它是C/C++函数默认的调用规范,C/C++运行时库中的函数基本都是__cdecl调用。在该调用约定下,参数从右向左依次压入栈中,由主调函数负责清理参数的栈空间。该调用约定适用于支持可变参数的函数,因为只有主调函数才知道给该种函数传递了多少个参数,才知道应该清理多少栈空间。比如支持可变参数的C函数printf:
int __cdecl printf ( const char *format, ... ) { va_list arglist; int buffing; int retval; _VALIDATE_RETURN( (format != NULL), EINVAL, -1); va_start(arglist, format); _lock_str2(1, stdout); __try { buffing = _stbuf(stdout); retval = _output_l(stdout,format,NULL,arglist); _ftbuf(buffing, stdout); } __finally { _unlock_str2(1, stdout); } return(retval); }
2.2、__stdcall标准调用
它是Windows系统提供的系统API函数的调用约定,比如API函数GetWindowText的声明如下:
WINUSERAPI int WINAPI GetWindowTextW( _In_ HWND hWnd, _Out_writes_(nMaxCount) LPWSTR lpString, _In_ int nMaxCount);
其中,WINAPI宏就是__stdcall标准调用,即:
#define WINAPI __stdcall
同时__stdcall也是很多提供给第三方使用的SDK库的API接口的调用约定。在该调用约定下,参数从右向左依次压入栈中,由被调用函数负责清理栈空间。如果函数是可变参的,函数的调用约定会自动转化为__cdecl调用。
2.3、__fastcall快速调用
该调用约定之所以被称作为快速调用,因为有部分参数可以通过寄存器直接传递,效率比较高。对于内存大小小于等于4字节的参数,直接使用ECX和EDX寄存器传递,剩余的参数则依次从右到左压入栈中通过栈传递,参数传递占用的栈空间由被调用函数清理。
2.4、__thiscall调用
__thiscall是C++中的非静态类成员函数的默认调用约定。该调用约定也用到了寄存器传参,在调用C++类的非静态成员函数时会传入当前类对象的地址,该地址通过ECX寄存器来传递的。在该调用约定下,函数的参数按照从右到左的顺序入栈,被调用的函数在返回前清理参数的栈空间。
3、调用约定不一致导致的软件异常问题
以前我们将C++开发的SDK库提供给第三方厂商做二次开发,第三方客户使用的是C#语言,即C#开发的程序去调用C++开发的SDK库,当时因为SDK头文件中声明的回调函数没有指定调用约定,导致程序出现异常崩溃的问题。
我们C++开发的SDK提供了设置消息回调的API接口,并给出了回调函数的声明,如下:
/* 函数功能:用于消息回发的回调函数指针(服务器主动推送的消息通过该回调函数推给上层) 参数:DWORD dwMsgId:消息id const unsigned char* pMsgBuf:消息中携带的数据buffer,buffer中的具体内容取决于消息id,参看消息id的头文件 DWORD dwMsgBufLen:消息中携带的数据buffer长度 返回值:void */ typedef void (*PMsgCallBackFunc)( DWORD dwMsgId, const unsigned char* pMsgBuf, DWORD dwMsgBufLen );
设置回调函数的接口如下:
// 设置业务消息回调接口 SDK_DLL_API void __stdcall SetMsgCallBack( IN PMsgCallBackFunc pMsgCallBackFunc );
回调函数的实现在上层的C#程序中,回调函数的调用在C++实现的SDK中,因为回调函数PMsgCallBackFunc在声明时没有指定函数调用约定,在C#程序中默认是__stdcall标准约定,所以在C#中编译时回调函数内部会清理栈空间。而回调函数是在C++ SDK中调用的,在SDK编译时默认是__cdecl调用,会在调用回调函数处的主调函数中释放栈空间,这样导致回调函数调用后,主调函数会释放一次栈空间,回调函数内部会释放一次栈空间,所以多释放了一次参数栈空间,导致了栈不平衡,导致程序运行出异常。
考虑跨语言调用的场景,SDK要提供标准的C接口。在SDK的头文件中,SDK导出接口要指定调用约定,回调函数的声明也要指定调用约定。
4、与调用约定相关的工程配置选项及/RTC编译选项
在Visual Studio创建的C++工程中,在没明确指定函数调用约定时,默认使用的都是__cdecl调用,我们可以在工程属性配置中看到:
对于C++工程,我们一般不需要修改默认的调用约定。如果要指定dll库导出接口的调用约定,我们也不需要修改工程配置,只需要在导出接口的头文件的函数声明处指定调用约定就可以了。
有人可能会说,工程属性配置中使用了默认的__cdecl调用,我们又在头文件中将接口指定为__stdcall标准调用,会不会有冲突?到底以哪个为准呢?没有冲突的,编译时是优先以接口声明处指定的调用约定为准的。
在Debug下/RTC运行时检测编译选项是默认开启的,/RTC运行时检测在函数调用完成后会去检测栈是否平衡,关于这一点的说明如下:(MSDN上对/RTC编译选项的说明)
如果没有释放参数的栈空间或者参数栈空间多释放了一次,都能检测出来。如果检测到,会弹出如下的提示:
到此这篇关于C/C++函数的调用约定的使用的文章就介绍到这了,更多相关C/C++函数调用约定内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!