嵌入式软件工程师面经C/C++篇—函数调用约定:一文看懂 __cdecl 和 __stdcall(超详细入门)

在 C/C++ 编程中,“函数调用约定” 是一套 “隐形规则”—— 它规定了函数调用时参数怎么压栈、谁来清理栈、函数名怎么修饰。如果不理解这套规则,可能会遇到 “编译通过但链接报错”“程序崩溃” 等奇怪问题,尤其是在调用 Windows API 或第三方库时。

本文聚焦最常用的两种调用约定:__cdecl 和 __stdcall,用通俗的语言和代码示例讲清楚它们的区别和用法。

一、先搞懂:什么是 “函数调用约定”?

简单来说,函数调用的过程就像 “寄快递”:

  • 调用者(比如 main 函数)是 “寄件人”,要把 “包裹”(参数)按规则交给快递员(栈);
  • 被调用者(比如 add 函数)是 “收件人”,取到包裹后处理;
  • 最后要有人负责 “清理快递盒”(栈空间)—— 这就是调用约定要解决的核心问题。

所有调用约定都会明确 3 件事:

  1. 参数压栈顺序:先压第一个参数,还是最后一个参数?
  2. 栈清理责任:调用者清理栈,还是被调用者清理栈?
  3. 函数名修饰规则:编译器给函数起的 “内部名字” 是什么样的(影响链接)?

二、__cdecl:C 语言默认的调用约定

__cdecl 是 “C Declaration” 的缩写,是 C/C++ 程序的默认调用约定(比如 printf 就用它)。

1. __cdecl 的核心规则

规则具体说明
参数压栈顺序从右到左(比如调用 add(1,2),先压 2,再压 1
栈清理责任调用者清理(寄件人自己收拾快递盒)
函数名修饰(VC 编译器)函数名前加下划线 _(比如 add 会变成 _add

2. 为什么是 “调用者清理栈”?

最大的好处是支持可变参数函数(比如 printf(const char* format, ...))。因为被调用者(比如 printf)不知道调用者会传几个参数(可能是 printf("%d", a),也可能是 printf("%d,%s", a, b)),无法确定要清理多少栈空间,所以只能让调用者(知道自己传了几个参数)来清理。

3. 代码示例 + 汇编解析(直观理解)

我们用一个简单的加法函数,看 __cdecl 是怎么工作的(基于 32 位环境,栈单位为 4 字节)。

C 代码:
#include <stdio.h>

// 显式声明 __cdecl 调用约定(默认也可以不写)
int __cdecl add_cdecl(int a, int b) {
    return a + b; // 返回值存在 EAX 寄存器
}

int main() {
    int result = add_cdecl(10, 20); // 调用函数
    printf("结果:%d\n", result);
    return 0;
}
对应的关键汇编代码(简化版):
; ------------------- main 函数(调用者)的操作 -------------------
push 20          ; 1. 先压右参数 b=20(栈地址:低地址→高地址)
push 10          ; 2. 再压左参数 a=10(此时栈中:[10, 20])
call add_cdecl    ; 3. 调用函数:把下一条指令地址压栈,跳转到 add_cdecl
add esp, 8        ; 4. 调用者清理栈!esp+8 表示清除 2 个参数(每个 4 字节)

; ------------------- add_cdecl 函数(被调用者)的操作 -------------------
add_cdecl:
    mov eax, [esp+4]  ; 从栈中取 a=10(esp+4 是第一个参数的位置)
    mov edx, [esp+8]  ; 从栈中取 b=20(esp+8 是第二个参数的位置)
    add eax, edx      ; 计算 a+b,结果存在 eax 中
    ret               ; 5. 仅返回:把栈中保存的指令地址弹出,跳回 main
关键观察:
  • 压栈顺序:20 先压,10 后压(右→左);
  • 栈清理:add esp, 8 是在 main 中执行的(调用者清理);
  • 返回值:通过 eax 寄存器带回 main

三、__stdcall:Windows API 常用的调用约定

__stdcall 是 “Standard Call” 的缩写,不是默认调用约定,但在 Windows API 中被广泛使用(比如 MessageBoxCreateWindow 等)。

1. __stdcall 的核心规则

规则具体说明
参数压栈顺序和 __cdecl 一样:从右到左
栈清理责任被调用者清理(收件人收拾快递盒)
函数名修饰(VC 编译器)函数名前加 _,后加 @+ 参数总字节数(比如 add 会变成 _add@8,8 是 2 个 int 的字节数)

2. 为什么是 “被调用者清理栈”?

好处是效率更高:调用者不需要写清理栈的代码,尤其在多次调用同一个函数时,能减少代码量。但缺点也明显:不支持可变参数—— 因为被调用者必须知道参数的总字节数,才能确定要清理多少栈空间(比如 ret 8 表示清理 8 字节)。

3. 代码示例 + 汇编解析

还是用加法函数,看 __stdcall 和 __cdecl 的区别。

C 代码:
#include <stdio.h>

// 显式声明 __stdcall 调用约定
int __stdcall add_stdcall(int a, int b) {
    return a + b;
}

int main() {
    int result = add_stdcall(10, 20); // 调用函数
    printf("结果:%d\n", result);
    return 0;
}
对应的关键汇编代码(简化版):
; ------------------- main 函数(调用者)的操作 -------------------
push 20          ; 1. 先压右参数 b=20(和 __cdecl 一样)
push 10          ; 2. 再压左参数 a=10
call add_stdcall ; 3. 调用函数:压指令地址,跳转
; 4. 没有栈清理代码!因为被调用者会清理

; ------------------- add_stdcall 函数(被调用者)的操作 -------------------
add_stdcall:
    mov eax, [esp+4]  ; 取 a=10(和 __cdecl 一样)
    mov edx, [esp+8]  ; 取 b=20
    add eax, edx      ; 计算 a+b
    ret 8             ; 5. 返回 + 清理栈!ret 8 表示:弹出指令地址后,esp+8(清理 8 字节)
关键观察:
  • 压栈顺序和 __cdecl 完全相同;
  • 栈清理:ret 8 是在 add_stdcall 中执行的(被调用者清理);
  • 函数名修饰:add_stdcall 会变成 _add_stdcall@8(比 __cdecl 多了 @8)。

四、__cdecl vs __stdcall:核心区别总结

对比维度__cdecl(C 默认)__stdcall(Windows API 常用)
栈清理责任调用者(寄件人)被调用者(收件人)
可变参数支持✅ 支持(如 printf❌ 不支持
函数名修饰(VC)前加 _(如 _add前加 _,后加 @字节数(如 _add@8
适用场景通用 C/C++ 代码、可变参数函数Windows API、固定参数的库函数
效率略低(调用者要写清理代码)略高(被调用者统一清理)

五、实际开发中的常见问题与注意事项

1. 问题 1:调用约定不匹配导致链接错误

比如你在代码中调用 Windows API MessageBox(它用 __stdcall),但如果你误把函数声明为 __cdecl,编译器会生成错误的函数名(比如 _MessageBoxA vs _MessageBoxA@16),链接时就会报 “无法解析的外部符号”。

解决办法:Windows API 已经通过宏定义好了调用约定,直接包含 windows.h 即可(比如 #define WINAPI __stdcall),不要自己改。

2. 问题 2:调用约定不匹配导致栈溢出

如果调用者按 __cdecl 写(不清理栈),但被调用者按 __stdcall 写(也清理栈),会导致栈被 “重复清理”;反之,如果调用者清理了,被调用者也清理了,会导致栈指针错误,最终程序崩溃。

解决办法:调用函数时,确保声明的调用约定和函数实际的调用约定一致(比如看库文档说明)。

3. 如何显式指定调用约定?

在函数声明时加关键字即可,示例:

// __cdecl 调用约定(可省略,默认)
int __cdecl add(int a, int b);

// __stdcall 调用约定(Windows API 常用)
int __stdcall MessageBoxA(void* hWnd, const char* text, const char* caption, unsigned int uType);

六、一句话总结(方便记忆)

  • __cdecl:C 家默认,调用者清栈,支持可变参数(如 printf);
  • __stdcall:Windows 爱用,被调用者清栈,固定参数更高效。

记住这两点,就能应对大多数开发场景啦!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值