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

 更新时间:2022年06月23日 15:09:27   作者:dvlinker  
本文主要介绍了C/C++函数的调用约定的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

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

您可能感兴趣的文章:

相关文章

  • C++中Overload,Override,Hide之间的区别

    C++中Overload,Override,Hide之间的区别

    重载overload,这个概念是大家熟知的。在同一可访问区内被声名的几个具有不同参数列的(参数的类型、个数、顺序不同)同名函数,程序会根据不同的参数列来确定具体调用哪个函数,这种机制就是重载
    2013-09-09
  • 使用C语言打印月历

    使用C语言打印月历

    这篇文章主要为大家详细介绍了使用C语言打印月历,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-06-06
  • C++实现LeetCode(144.二叉树的先序遍历)

    C++实现LeetCode(144.二叉树的先序遍历)

    这篇文章主要介绍了C++实现LeetCode(144.二叉树的先序遍历),本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-07-07
  • c语言版本二叉树基本操作示例(先序 递归 非递归)

    c语言版本二叉树基本操作示例(先序 递归 非递归)

    这篇文章主要介绍了实现二叉树的创建(先序)、递归及非递归的先、中、后序遍历
    2013-11-11
  • 实例讲解在C++的函数中变量参数及默认参数的使用

    实例讲解在C++的函数中变量参数及默认参数的使用

    这篇文章主要介绍了在C++的函数中变量参数及默认参数的使用,是C++函数入门学习中的基础知识,需要的朋友可以参考下
    2016-01-01
  • C语言利用栈实现对后缀表达式的求解

    C语言利用栈实现对后缀表达式的求解

    这篇文章主要为大家详细介绍了C语言利用栈实现对后缀表达式的求解,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-04-04
  • C++中复制构造函数和重载赋值操作符总结

    C++中复制构造函数和重载赋值操作符总结

    这篇文章主要介绍了C++中复制构造函数和重载赋值操作符总结,本文对复制构造函数和重载赋值操作符的定义、调用时机、实现要点、细节等做了总结,需要的朋友可以参考下
    2014-10-10
  • C++基于消息队列的多线程实现示例代码

    C++基于消息队列的多线程实现示例代码

    这篇文章主要给大家介绍了关于C++基于消息队列的多线程实现的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用C++具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-04-04
  • C语言函数栈帧的创建和销毁介绍

    C语言函数栈帧的创建和销毁介绍

    大家好,本篇文章主要讲的是C语言函数栈帧的创建和销毁介绍,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下
    2021-12-12
  • oaptt搭建http服务的过程详解

    oaptt搭建http服务的过程详解

    这篇文章主要介绍了oaptt搭建http服务,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-03-03

最新评论