全局变量:为什么“=0”很重要?

Global variable: why "=0" matters?

场景:

$ cat t0.c t1.c
/* t0.c */
int i = 12;
/* t1.c */
int i INIT;

int main(void)
{
        return 0;
}

$ gcc t0.c t1.c -DINIT="" -std=c11 -pedantic
<nothing>

$ gcc t0.c t1.c -DINIT="=0" -std=c11 -pedantic
ld: /tmp/ccrTTgwH.o:(.bss+0x0): multiple definition of `i'; /tmp/cckd6R4u.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status

为什么 =0 很重要?

更新:

dvl-linux64 $ gcc82 --version
gcc-8.2 (GCC) 8.2.0

尝试 gcc 11.2.0:

$ gcc t0.c t1.c -DINIT="" -std=c11 -pedantic
ld: /tmp/ccmPBPUT.o:t1.c:(.bss+0x0): multiple definition of `i'; 
$ gcc t0.c t1.c -DINIT="=0" -std=c11 -pedantic
ld: /tmp/ccxw378s.o:t1.c:(.bss+0x0): multiple definition of `i';

预期。

最终 table:

compiler          -DINIT="" leads to multiple definition of `i'?
gcc 8.2.0         NO
gcc 11.2.0        YES
clang 8.0.1       NO
clang 13.0.0      YES

就C标准而言,两者都是无效的,因为它们构成了一个标识符的多个外部定义。这实际上是 undefined behavior,但是大多数链接器在这种情况下会抛出错误。

在 t1.c 中的 int i; 的情况下,这被认为是 暂定定义 因为它没有初始值设定项。但是,如果同一翻译单元中没有其他定义,则此暂定定义将被视为完整定义。

这在 C standard 的第 6.9.2p2 节中有描述:

A declaration of an identifier for an object that has file scope without an initializer, and without a storage-class specifier or with the storage-class specifier static, constitutes a tentative definition. If a translation unit contains one or more tentative definitions for an identifier, and the translation unit contains no external definition for that identifier, then the behavior is exactly as if the translation unit contains a file scope declaration of that identifier, with the composite type as of the end of the translation unit, with an initializer equal to 0.

但是,某些编译器(例如 gcc)会采用暂定定义并将其与其他目标文件中的实际定义“合并”。一旦你在多个目标文件中有一个完整的定义(即带有一个初始化器)(即使初始化器是相同的),那么它就会产生一个多重定义错误。

-fcommon 选项启用此行为,-fno-common 在 gcc 中禁用它,尽管默认值取决于版本。