【问题标题】:clang behaves differently with global variablesclang 对全局变量的行为不同
【发布时间】:2021-09-07 22:58:13
【问题描述】:

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

测试.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,它会破坏与每个编译器的链接(除非我为@ 添加定义987654331@ 当然是在源文件中)。

有些东西我不明白。发生了什么?

【问题讨论】:

  • omplaining about gv already define 请发布确切的完整编译器错误消息,包括所有通知、行号和文件名。请发布您正在使用的编译器选项和编译器版本。你想研究常见的符号,比如stackoverflow.com/a/15604964/9072753
  • 编译器对gv 范围界定的方式不同。 (即它在文件中的可见性。)如果您将gv 声明为extern,即extern int gv;。然后在main()中定义gv,即int gv = 0;,那么它在两个编译器中的工作方式可能相同。
  • 根据@KamilCuk链接,这似乎只是一个UB案例,由于一些编译器扩展,它在某些平台上有效。

标签: c linker storage-class-specifier


【解决方案1】:

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

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

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

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

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

一些附加信息是herehere

【讨论】:

    【解决方案2】:

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

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

    这是因为test.cmain.c 在预处理器包含标头后都有int gv;。因此最终test.omain.o 两个对象都包含_gv 符号。

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


    编辑:

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

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

    【讨论】:

      猜你喜欢
      • 2021-11-06
      • 1970-01-01
      • 2015-04-12
      • 1970-01-01
      • 2020-12-19
      • 2012-09-28
      • 1970-01-01
      • 1970-01-01
      • 2021-02-24
      相关资源
      最近更新 更多