0

我有这些由 3 个文件组成的虚拟软件:

测试.h

int gv;
void set(int v);

测试.c

#include "test.h"

void set(int x) {
    gv = x;
}

主程序

#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是在文件范围内定义的对象的默认存储类说明符,但如果我明确指定externto int gv,它会破坏与每个编译器的链接(gv当然,除非我在源文件中添加定义)。

有一点我不明白。怎么了?

4

2 回答 2

1

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

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

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

要解决此问题,您可以更改int gv;test.hextern int gv;这使其成为不是定义的声明(甚至不是暂定定义)。然后你应该输入int gv;int gv = 0;test.c程序提供一个定义。另一种解决方案可能是使用-fcommon开关,如下所示。

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

一些附加信息在这里这里

于 2021-06-24T13:13:14.893 回答
0

我知道 extern 是在文件范围内定义的对象的默认存储类说明符

这是真的,但是由于gv符号的“重新定义”而导致链接中断,不是吗?

那是因为预处理器后两者test.cmain.c包含int gv;标头。因此最终既包含对象test.omain.o包含_gv符号。

最常见的解决方案是extern int gv;test.h头文件中包含(它告诉编译器gv存储分配在其他地方)。main.c例如,在 C 文件内部定义int gv;,以便在对象gv内部实际分配存储空间,但只分配一次main.o


编辑:

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

带有外部链接的声明通常在头文件中可用,以便所有#include 文件的翻译单元可以引用在别处定义的相同标识符。

于 2021-06-24T12:57:20.747 回答