调用约定不一致引发“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种组合,分别做了测试,得到不同的报错信息:

调用约定生成代码报错提示
__cdeclC无法解析的外部符号 _api_call
__stdcallC无法解析的外部符号 _api_call@4
__fastcallC无法解析的外部符号 @api_call@4
__cdeclC++无法解析的外部符号 “int __cdecl api_call(int)” (?api_call@@YAHH@Z)
__stdcallC++无法解析的外部符号 “int __fastcall api_call(int)” (?api_call@@YIHH@Z)
__fastcallC++无法解析的外部符号 “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);