clang 对全局变量的行为不同

clang behaves differently with global variables

我有这些由 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)在 link 时失败抱怨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,但是如果我明确指定 externint gv,它会破坏 link与每个编译器一起使用(当然,除非我在源文件中添加 gv 的定义)。

有些事情我不明白。发生了什么事?

I understand that extern is the default storage-class specifier for objects defined at file scope

确实如此,但是 link 年龄因 gv 符号的“重新定义”而中断,不是吗?

这是因为 test.cmain.c 在预处理器包含 header 之后都有 int gv;。因此最终 objects test.omain.o 都包含 _gv 符号。

最常见的解决方案是在 test.h header 文件中包含 extern int gv;(它告诉编译器 gv 存储分配在其他地方)。在 C 文件中,例如 main.c,定义 int gv;,这样 gv 的存储将实际分配一次,但只在 main.o object 中分配一次。


编辑:

引用您提供的相同 link storage-class specifier,其中包含以下语句:

Declarations with external linkage are commonly made available in header files so that all translation units that #include the file may refer to the same identifier that are defined elsewhere.

int gv;gv 暂定定义 ,根据 C 2018 6.9.2 2. 当翻译单元中没有常规定义时 (正在编译的文件及其包含的所有内容),暂定定义成为初始值设定项为零的定义。

由于test.cmain.c都包含这个暂定定义,所以test.cmain.c中都有暂定定义。当这些链接在一起时,您的程序有两个定义。

C 标准没有定义同一标识符有两个具有外部链接的定义时的行为。 (有两个定义违反了 C 2018 6.9 5 中的“应”要求,标准没有定义违反要求时的行为。)由于历史原因,一些编译器和链接器将暂定定义视为“公共符号”定义,将由链接器合并——具有相同符号的多个暂定定义将被解析为一个定义。有些人没有;有些将暂定定义更多地视为常规定义,如果有多个定义,链接器会抱怨。这就是为什么您会看到不同编译器之间存在差异的原因。

要解决这个问题,你可以把test.h中的int gv;改成extern int gv;,这样就变成了一个非定义的声明(甚至不是暂定的定义)。那么你应该把int gv;int gv = 0;放在test.c中,为程序提供一个定义。另一种解决方案可能是使用 -fcommon 开关,如下所示。

默认行为在 GCC 版本 10 中发生了变化(在某些时候可能还有 Clang;我的 Apple Clang 11 的行为与您的报告不同)。使用 GCC 和 Clang,您可以 select 使用命令行开关 -fcommon(将暂定定义视为通用符号)或 -fno-common(如果存在链接器错误)所需的行为多个暂定定义)。

一些附加信息是 here and