调用约定不一致引发“error LNK2019: 无法解析的外部符号“
前言
Visual Studio 编译选项有3种调用约定:__cdecl、__stdcall、__fastcall
__cdecl是默认的调用约定,也是C语言的标准调用约定。 __stdcall是Windows API使用的调用约定。 __fastcall用得比较少。
三种调用约定的细节,在网上有更详细的说明,这里就不解释了。 参考官方文档:https://docs.microsoft.com/zh-cn/cpp/cpp/argument-passing-and-naming-conventions?view=msvc-170
问题还原:
现在有这样一个VC++工程,有3个源文件:
main.c api.h api.c
main.c的源码如下:
#include "api.h"
int main(void)
{
int ret;
ret = api_call(0);
return ret;
}
api.h的源码如下:
#pragma once
int api_call(int a);
api.c的源码如下:
#include "api.h"
int api_call(int a)
{
int b;
b = a + 1;
return b;
}
这是一个非常简单的演示程序,在main函数调用api.c提供的接口。 正常情况下编译也不会有任何问题。
问题在于有一次不小心修改了编译选项: 在VS工程目录上点击api.c右键 -> 属性 -> C/C++ -> 高级 -> 调用约定 -> 选择__stdcall 并且保存后忘记改回去,过了很久很久之后,重新打开这个工程,编译报错了:
无法解析的外部符号 “int __cdecl api_call(int)” (?api_call@@YAHH@Z),该符号在函数 _main 中被引用
然后陷入深思,这怎么会报错?
问题分析:
由于VS支持多种调用约定,为了避免调用错误,编译器针对不同的调用约定,在编译的时候对函数名进行了重命名。 参考文档:修饰名 https://docs.microsoft.com/zh-cn/cpp/build/reference/decorated-names 新建工程时默认配置是__cdecl调用约定,并且C语言的main函数必须是__cdecl调用约定。 调用api_call的时候,按照__cdecl约定,它应该被改名为:?api_call@@YAHH@Z 但是修改api.c的编译选项为__stdcall之后,函数被改名为:?api_call@@YGHH@Z 因此最终链接时就会出现找不到?api_call@@YAHH@Z的错误。
下面针对编译选项的6种组合,分别做了测试,得到不同的报错信息:
| 调用约定 | 生成代码 | 报错提示 |
|---|---|---|
| __cdecl | C | 无法解析的外部符号 _api_call |
| __stdcall | C | 无法解析的外部符号 _api_call@4 |
| __fastcall | C | 无法解析的外部符号 @api_call@4 |
| __cdecl | C++ | 无法解析的外部符号 “int __cdecl api_call(int)” (?api_call@@YAHH@Z) |
| __stdcall | C++ | 无法解析的外部符号 “int __fastcall api_call(int)” (?api_call@@YIHH@Z) |
| __fastcall | C++ | 无法解析的外部符号 “int __stdcall api_call(int)” (?api_call@@YGHH@Z) |
这样的问题在调用第三方静态库(*.lib)时也可能出现。
比如生成*.lib时,是以__stdcall方式编译的,而调用lib中的函数时,是以__cdecl方式编译,也会出现类似的问题。
解决方案:
方案1. 全部代码都是我写的:找到编译选项不一致的文件,修改它的属性,使用默认值进行编译。
方案2. 我要引用第三方静态库:修改工程默认配置,改为与静态库一致的调用约定。
方案3. 我要生成静态库给别人用:最好使用默认的约定__cdecl。
方案4. 适用所有情况:在头文件(*.h)中声明接口函数时,像下面这样指定调用约定。
int __cdecl api_call(int a);
int __stdcall api_call(int a);
int __fastcall api_call(int a);