__stdcall由被调用函数清理栈,__cdecl由调用方清理;Windows API用__stdcall,C标准库用__cdecl;约定不一致会导致栈失衡、崩溃或链接失败。

__stdcall 和 __cdecl 的栈清理责任不同
核心区别在于谁负责清理调用栈上的参数:__cdecl 由调用方清理,__stdcall 由被调用函数自己清理。这意味着同一个函数声明,如果调用方和被调用方约定不一致,ESP 会错位,轻则逻辑异常,重则直接崩溃——尤其在回调、动态链接或跨模块调用时特别危险。
常见错误现象:Access violation reading location、函数返回后栈指针错乱、局部变量值“随机”改变、调试时发现 ESP 在函数返回前/后不匹配。
- Windows API 函数(如
MessageBoxA、CreateThread)全部使用__stdcall,所以你写回调函数(比如线程入口、窗口过程WndProc)必须显式声明为__stdcall,否则系统调用你的函数后无法正确弹栈 - C/C++ 标准库函数(如
printf、malloc)默认是__cdecl,支持可变参数;而__stdcall不允许可变参数(编译器直接报错:error C2733: second C linkage of overloaded function 'xxx' not allowed) - 在头文件中混用约定易被忽略:比如你自己写的 DLL 导出函数声明为
__cdecl,但头文件没加修饰,调用方按默认__cdecl使用没问题;一旦某天你改成__stdcall却忘了同步更新头文件里的声明,调用方仍按__cdecl调用,就出问题
名字修饰(Name Mangling)规则完全不同
调用约定直接影响编译器生成的符号名,这是链接能否成功的关键。VC++ 下:__cdecl 函数修饰名为 _funcname(前面单下划线),__stdcall 是 _funcname@N(后面带 @+ 参数字节数)。
典型场景:手动 GetProcAddress 加载 DLL 中的函数时,传错名字就会返回 NULL:
立即学习“C++免费学习笔记(深入)”;
// 假设 DLL 中导出的是 __stdcall int add(int a, int b); // 正确写法(注意 @8 表示两个 int,共 8 字节) FARPROC p = GetProcAddress(hMod, "add@8"); // 错误写法:用 "_add" 或 "add" 都找不到 FARPROC p = GetProcAddress(hMod, "_add"); // 找不到 FARPROC p = GetProcAddress(hMod, "add"); // 找不到
- 使用
dumpbin /exports xxx.dll可查看实际导出的修饰名,别猜 - 用
extern "C"可禁用 C++ 名字修饰,但不会改变调用约定;必须同时加__stdcall或__cdecl才有效 - .def 文件里写裸函数名(如
add)时,链接器会自动按模块默认约定映射;但如果模块默认是__cdecl,而函数实际是__stdcall,仍需在 .def 中写add@8显式指定
混合使用时的链接与运行时风险
同一个工程里混用两种约定本身合法,但危险点在于「隐式依赖」:比如静态库 A 编译时用 __cdecl,你把它链接进一个默认 __stdcall 的 Win32 项目(比如 MFC DLL),只要没直接调用 A 里的函数,可能一直不暴露问题;一旦调用,栈就不平衡。
- 检查方法:在函数入口加断点,观察
ESP值;调用前后对比,若差值 ≠ 参数总字节数(对__cdecl还要看调用方是否清理),说明约定错配 - VS 项目设置里可以统一指定默认调用约定(
/Gd= __cdecl,/Gz= __stdcall),但仅影响未显式声明的函数;已有__stdcall修饰的函数不受影响 - 第三方库文档没写调用约定?优先查其头文件——看函数声明有没有
WINAPI(即#define WINAPI __stdcall)或CDECL宏;没有就反汇编或用Dependency Walker看导出符号格式
现代 C++ 开发中哪些地方还必须关心这个
纯新项目用不到,但维护旧代码、对接 Windows 底层、写驱动或逆向分析时绕不开。尤其要注意:C++ 成员函数隐式传 this 指针,其调用约定不是 __cdecl 或 __stdcall,而是 __thiscall(VC++ 默认),它把 this 放 ECX,其余参数按 __cdecl 规则压栈——所以不能把普通函数指针强制转成成员函数指针,也不能用 std::function 直接绑定裸 __stdcall 函数(类型不匹配)。
- WinAPI 回调必须用
static成员函数或全局函数 +__stdcall,不能用普通成员函数(__thiscall无法被系统调用) - COM 接口所有函数都是
__stdcall,且第一个参数永远是IUnknown*,这是硬性 ABI 要求 - 写内联汇编时,若函数声明为
__stdcall,你手写的ret 8就得匹配参数大小;写成ret就等同于__cdecl行为,必然崩


















