问题描述
我有这些由 3 个文件组成的虚拟软件:
test.h
int gv;
void set(int v);
test.c
#include "test.h"
void set(int x) {
gv = x;
}
main.c
#include "test.h"
#include <assert.h>
int main() {
set(1);
assert(gv == 1);
}
代码在 MSVC 2019 和 GCC 8 中编译并运行良好,但使用 clang(Visual Studio 2019 提供的 clang-cl 11)在链接时失败,抱怨 gv
已定义:
1>------ Build started: Project: test,Configuration: Debug x64 ------
1>lld-link : error : undefined symbol: gv
1>>>> referenced by ...\test\main.c:6
1>>>> x64\Debug\main.obj:(main)
1>>>> referenced by ...\test\test.c:4
1>>>> x64\Debug\test.obj:(set)
1>Done building project "test.vcxproj" -- FAILED.
我知道 extern
是在文件范围内定义的对象的默认 storage-class specifier,但是如果我明确指定 extern
到 int gv
,它会中断与每个编译器的链接(当然,除非我在源文件中为 gv
添加定义)。
有一点我不明白。发生了什么?
解决方法
int gv;
是 gv
的暂定定义,根据 C 2018 6.9.2 2. 当翻译单元中没有常规定义时(正在编译的文件)包括它所包含的一切),一个临时定义变成了一个初始化器为零的定义。
由于此暂定定义包含在 test.c
和 main.c
中,因此 test.c
和 main.c
中都有暂定定义。当这些链接在一起时,您的程序就有两个定义。
C 标准没有定义当具有外部链接的相同标识符有两个定义时的行为。 (有两个定义违反了 C 2018 6.9 5 中的“shall”要求,并且标准没有定义违反要求时的行为。)由于历史原因,一些编译器和链接器将暂定定义视为“公共符号”定义将被链接器合并——具有相同符号的多个暂定定义将被解析为单个定义。而有些则没有;有些人将暂定定义视为常规定义,如果有多个定义,链接器会抱怨。这就是您看到不同编译器之间存在差异的原因。
要解决此问题,您可以将 int gv;
中的 test.h
更改为 extern int gv;
,这使其成为不是定义(甚至不是临时定义)的声明。然后您应该将 int gv;
或 int gv = 0;
放在 test.c
中以提供程序的一个定义。另一种解决方案是使用 -fcommon
开关,如下所示。
默认行为在 GCC 版本 10 中发生了变化(在某些时候可能还有 Clang;我的 Apple Clang 11 的行为与您的报告不同)。使用 GCC 和 Clang,您可以使用命令行开关 -fcommon
(将暂定定义视为公共符号)或 -fno-common
(如果有多个暂定定义导致链接器错误)选择所需的行为.
我知道 extern 是在文件范围内定义的对象的默认存储类说明符
确实如此,但链接因 gv
符号的“重新定义”而中断,不是吗?
这是因为 test.c
和 main.c
在预处理器包含标头后都有 int gv;
。因此最终对象 test.o
和 main.o
都包含 _gv
符号。
最常见的解决方案是在 extern int gv;
头文件中包含 test.h
(它告诉编译器 gv
存储分配在其他地方)。在 C 文件中,例如 main.c
,定义 int gv;
以便 gv
的存储将实际分配,但仅在 main.o
对象内部分配一次。
编辑:
引用您提供的相同链接 storage-class specifier,其中包含以下声明:
带有外部链接的声明通常在头文件中可用,以便所有 #include 文件的翻译单元可以引用在别处定义的相同标识符。