C/C++函数的调用约定的使用

目录
  • 1、概述
  • 2、常见的调用约定说明
    • 2.1、__cdecl C调用
    • 2.2、__stdcall标准调用
    • 2.3、__fastcall快速调用
    • 2.4、__thiscall调用
  • 3、调用约定不一致导致的软件异常问题
  • 4、与调用约定相关的工程配置选项及/RTC编译选项

函数的调用约定其实比较简单,并不复杂,但很多人对这一块内容不太了解,甚至连工作几年的朋友也不太清楚。最近有朋友想了解这一块的内容,所以今天我们就来讲一下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++函数调用约定内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

时间: 2022-06-23

C和C++的函数调用约定你知道多少

目录 调用方式 1.__cdecl 2.__stdcall 3.__fastcall 4.naked 5.__pascal 6.__thiscall 名字修饰约定 1.C编译时函数名修饰约定规则 2.C++编译时函数名修饰约定规则 总结 调用方式 C/C++函数有多种调用约定. C语言: __cdecl __stdcall __fastcall naked __pascal C++比C语言多了一种: __thiscall 1. __cdecl __cdecl调用约定又称为C调用约定,时C/C++

Python调用C/C++动态链接库的方法详解

本文以实例讲解了Python调用C/C++ DLL动态链接库的方法,具体示例如下: 示例一: 首先,在创建一个DLL工程(本例创建环境为VS 2005),头文件: //hello.h #ifdef EXPORT_HELLO_DLL #define HELLO_API __declspec(dllexport) #else #define HELLO_API __declspec(dllimport) #endif extern "C" { HELLO_API int IntAdd(in

详解java模板和回调机制

最近看spring的JDBCTemplete的模板方式调用时,对模板和回调产生了浓厚兴趣,查询了一些资料,做一些总结. 回调函数: 所谓回调,就是客户程序C调用服务程序S中的某个函数A,然后S又在某个时候反过来调用C中的某个函数B,对于C来说,这个B便叫做回调函数.回调函数只是一个功能片段,由用户按照回调函数调用约定来实现的一个函数.回调函数是一个工作流的一部分,由工作流来决定函数的调用(回调)时机.一般说来,C不会自己调用B,C提供B的目的就是让S来调用它,而且是C不得不提供.由于S并不知道C

C#通过PInvoke调用c++函数的备忘录的实例详解

目前知道的情况被调用的C/C++函数只能是全局函数 不能调用类中的成员方法 被调用的C函数必须使用extern "C"包含,保证采用的导出函数名生成规则和.NET一致 函数调用约定通常使用WINAPI也就是__stdcall,.net默认也是__stdcall .net可以和c++同时用cdecl调用约定,这样可以支持可变参数个数 c函数必须使用__declspec(dllexport)前缀来导出 PInvoke assistant工具可以辅助生成C#和VB的引入声明,还可以查看常见的

Python嵌入C/C++进行开发详解

如果你想把Python嵌入C/C++中是比较简单的事情,你需要的是在VC中添加Python的include文件目录和lib文件目录.下面我们来看下如何把Python嵌入C/C++中. VC6.0下,打开 tools->options->directories->show directories for,将Python安装目录下的inlude目录添加到inlude files项中,将libs目录添加到library files项中. VC2005下,打开tools->options-

C/C++函数调用的几种方式总结

调用函数时,计算机常用栈来存储传递给函数的参数. 栈是一种先进后出的数据结构,栈有一个存储区.一个栈顶指针.栈顶指针指向堆栈中第一个可用的数据项(被称为栈顶).用户可以在栈顶上方向栈中加入数据,这个操作被称为压栈(Push),压栈以后,栈顶自动变成新加入数据项的位置,栈顶指针也随之修改.用户也可以从堆栈中取走栈顶,称为弹出栈(pop),弹出栈后,栈顶下的一个元素变成栈顶,栈顶指针随之修改.函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算.函数计算结束以

汇编 函数调用的实现

1. 从代码的顺序执行说起 每一个程序员脑子里应该都有这么一种印象:"程序是顺序执行的".这个观点其实和我们开篇所讲的cpu的流水线执行过程直接相关. 让我们再回忆一下脑海中关于函数调用的概念,也许会是这个样子: 这里的"控制流转移"又是如何发生的呢?在解释这个之前,也许我们需要科普一点有关于汇编的知识. 2. 函数调用中的一些细节说明 2.1 函数调用中的关键寄存器 2.1.1 程序计数器PC 程序计数器是一个计算机组成原理中讲过的概念,下面给出一个百度百科中的简

js 函数调用模式小结

方法调用模式 当一个函数被保存为对象的一个属性时,我们称之它为该对象的一个方法,那么this被绑定到该对象上. 复制代码 代码如下: var myObject={ name : "myObject" , value : 0 , increment : function(num){ this.value += typeof(num) === 'number' ? num : 0; } , toString : function(){ return '[Object:'+this.name

js函数调用的方式

Js函数调用的方式有如下几种情况: (1)具名函数直接调用 复制代码 代码如下: function foo()  {  }  foo(); (2)匿名函数通过引用来调用 复制代码 代码如下: fooRef = function()  {  }fooRef(); (3)没有引用的匿名函数调用1 复制代码 代码如下: (function() {}()); (4)没有引用的匿名函数调用2 复制代码 代码如下: (function() { })(); (5)没有引用的匿名函数调用3  复制代码 代码如下

js函数调用常用方法详解

来源 javascript语言精粹.这不是书上的源代码. js的函数调用会免费奉送两个而外的参数就是 this 和 arguments .arguments是参数组,他并不是一个真实的数组,但是可以使用.length方法获得长度. 书上有说4中调用方式: 方法调用模式 函数调用模式 构造器调用模式 apply调用模式 下面我们来看看一些实例更好理解. 1:方法调用模式 请注意this此时指向myobject. 复制代码 代码如下: /*方法调用模式*/ var myobject={ value:

vbs(asp) ByVal ByRef函数调用使用说明

1.ByVal传值:一种将参数值而不是将地址传递给过程的方式,这就使过程访问到变量的复本.结果,过程不可改变变量的真正值.  2.ByRef传值:一种将参数地址而不是将值传递给过程的方式,这就使过程访问到实际的变量.结果,过程可改变变量的真正值.除非另作说明,否则按地址传递参数.   3.系统默认的是ByRef传值. 例子: 复制代码 代码如下: <SCRIPT LANGUAGE="vbScript"> dim a a=0 document.write "a=0&