C++主流调用约定有__cdecl、__stdcall、__fastcall、__thiscall和__vectorcall;其中__cdecl由调用方清栈且支持可变参数,__stdcall由被调用方清栈且用于Win32 API,二者因栈清理责任和名字修饰不同导致ABI不兼容。

在 C++ 中,函数调用约定(Calling Convention)决定了函数调用时参数如何传递、栈由谁清理、函数名如何修饰等底层行为。这些约定直接影响二进制兼容性、跨语言调用(如 C++ 调用 Windows API 或 DLL)、以及汇编层的执行逻辑。
常见的 C++ 调用约定有哪些
主流调用约定包括:__cdecl、__stdcall、__fastcall、__thiscall(MSVC 特有)、__vectorcall(VS2013+,用于向量化参数)。其中 __cdecl 和 __stdcall 是最基础、最常被对比的两种,尤其在 Win32 平台和 ABI 兼容场景中至关重要。
__cdecl:C 语言默认约定,调用方清栈
这是 Microsoft Visual C++ 的默认调用约定(对普通 C/C++ 函数),也对应大多数 Unix-like 平台的 System V ABI(尽管命名不同,逻辑相似)。
- 参数从右到左压栈(即最后一个参数先入栈)
- 调用方(caller)负责清理栈空间 —— 这是关键区别:编译器在 call 指令后插入 add esp, N 指令,释放参数占用的栈空间
- 支持可变参数函数(如 printf),因为只有调用方知道实际传了多少参数,才能正确清理
- 函数名修饰为 _funcname(带前导下划线,无 @ 后缀)
__stdcall:Windows API 标准,被调用方清栈
这是 Win32 API 函数(如 MessageBoxA、CreateFileW)使用的约定,也是 COM 接口的标准之一。
立即学习“C++免费学习笔记(深入)”;
- 参数同样从右到左压栈
- 被调用方(callee)负责清理栈 —— 函数返回前执行 ret N(例如 ret 8 表示清理 8 字节参数),栈平衡由函数自身保证
- 不支持可变参数(语法上允许声明,但链接或调用会出错;编译器通常报错)
- 函数名修饰为 _funcname@N(前导下划线 + @ + 总参数字节数,如 _DrawTextA@20)
为什么栈清理责任不同会导致不兼容
本质是 ABI(Application Binary Interface)层面的契约断裂:
- 若声明为 __stdcall 但按 __cdecl 调用:调用方不清理栈 → 每次调用栈指针 esp 偏移累积 → 栈溢出或后续变量访问错乱
- 若声明为 __cdecl 但按 __stdcall 调用:函数内部执行 ret 12 清理,但实际只压了 4 字节 → esp 被过度修正 → 栈被“掏空”,返回地址错位 → 程序崩溃
- 名字修饰不匹配 → 链接器找不到符号(LNK2019),因为 _foo@8 和 _foo 被视为两个不同函数
其他约定简要对比
__fastcall:前两个 DWORD(或寄存器宽度)参数通过 ECX、EDX 传递,其余压栈,callee 清栈;名字修饰为 @funcname@N。目标是减少访存,提升小函数性能。
__thiscall(MSVC 成员函数默认):this 指针放 ECX,其余参数从右到左压栈,callee 清栈(除可变参成员函数,此时 caller 清栈);名字修饰类似 __cdecl 或 __stdcall,取决于是否含可变参。
System V AMD64 ABI(Linux/macOS/x64 Windows):已统一为寄存器传参(RDI, RSI, RDX, RCX, R8, R9, R10, R11),浮点用 XMM0–XMM7,栈仅作备用和对齐;caller 清栈(但无需显式 add rsp),无名字修饰差异(C++ 仍需 mangling,但与调用约定无关)。











