C和C++中的全局变量有什么区别?

28

我已经测试了以下代码:

在文件 a.c/a.cpp 中。

int a;

在文件b.c/b.cpp

int a;
int main() { return 0; }

我用gcc *.c -o test编译源文件时成功了。

但是当我使用g++ *.c -o test编译源文件时,会失败:

ccIJdJPe.o:b.cpp:(.bss+0x0): multiple definition of 'a'
ccOSsV4n.o:a.cpp:(.bss+0x0): first defined here
collect2.exe: error: ld returned 1 exit status

我真的很困惑这个问题。在C和C++中的全局变量有什么区别吗?


2
C++:在头文件中放置“extern int a;”,并且在源文件中仅声明“int a;”一次。否则,您将创建具有相同名称和作用域的两个不同变量,这是链接器错误。 - Neil Kirk
只需在文件 b.c/b.cpp 中进行声明 extern int a,而不是 int a,它就可以工作了。 - Uchia Itachi
1
我不明白为什么你在 C 语言中没有得到链接器错误。你基本上是在尝试声明两个具有相同名称的全局变量。 - user180247
8
@NeilKirk,是的,我知道如何纠正它。但是我对 C 和 C++ 中不同的行为感到困惑。 - sunlight07
2个回答

31
以下是标准的相关部分。请看下面标准文本的解释:
§6.9.2/2 外部对象定义
具有文件作用域且没有初始化程序、没有存储类说明符或具有静态存储类说明符的标识符的声明构成了一种试探性定义。如果一个翻译单元包含一个或多个标识符的试探性定义,并且该翻译单元不包含该标识符的外部定义,则行为就像该翻译单元包含该标识符的文件作用域声明,其组合类型为翻译单元末尾的组合类型,初始化程序等于0。
ISO C99 §6.9/5 外部定义
外部定义是函数(而不是内联定义)或对象的外部声明,也是定义。如果使用具有外部链接的标识符在表达式中(除了作为sizeof运算符的操作数之一,其结果是整数常量),那么整个程序中必须恰好有一个标识符的外部定义;否则,不能有多个。
使用C版本时,“g”全局变量将“合并”为一个,因此最终您只会有一个被声明两次的变量。这是可以的,因为在extern不需要的时候,或者可能不存在的情况下。因此,这是为了构建旧代码的历史和兼容性原因。这是gcc的一个扩展,用于此遗留功能。
它基本上使gcc为名为'a'的变量分配内存,因此可以有多个声明,但只有一个定义。这就是为什么即使使用gcc,下面的代码也不起作用。
这也称为试探性定义。C++中没有这样的东西,这就是为什么它可以编译的原因。C++没有试探性声明的概念。
试探性定义是任何没有存储类说明符和初始化程序的外部数据声明。如果到达翻译单元的末尾并且没有出现具有标识符的初始化程序的定义,则试探性定义成为完整定义。在这种情况下,编译器为定义的对象保留未初始化的空间。
请注意,即使使用gcc,以下代码也无法编译,因为这不再是试探性定义/声明,并且已分配值:
int a = 1;

在文件“b.c/b.cpp”中

int a = 2;
int main() { return 0; }

让我们通过更多的例子来深入了解。以下语句显示正常定义和暂定定义。请注意,静态将使其有所不同,因为它是文件范围,并且不再是外部的。

int i1 = 10;         /* definition, external linkage */
static int i2 = 20;  /* definition, internal linkage */
extern int i3 = 30;  /* definition, external linkage */
int i4;              /* tentative definition, external linkage */
static int i5;       /* tentative definition, internal linkage */
 
int i1;              /* valid tentative definition */
int i2;              /* not legal, linkage disagreement with previous */
int i3;              /* valid tentative definition */
int i4;              /* valid tentative definition */
int i5;              /* not legal, linkage disagreement with previous */

进一步的详细信息可以在以下页面找到:

http://c0x.coding-guidelines.com/6.9.2.html

此外,关于更多详情,请查看此博客文章:

http://ninjalj.blogspot.co.uk/2011/10/tentative-definitions-in-c.html


4
如您所引用的话所述,试声明与完整声明“在翻译单元的结尾”是完全相同的。这意味着每个翻译单元仍包含a的定义,并且程序仍存在错误,因为它违反了6.9/5。gcc之所以接受该代码,是因为它实现了标准的扩展。 - CB Bailey
@Charles:谢谢,我对我的版本进行了小幅更新。您还缺少什么来改进这个答案吗? - László Papp
是的,你留了一条评论,我已经修复了它。由于这将被许多浏览谷歌的人看到,还有其他建议吗? - László Papp
8
我认为答案是误导性的。您引用并讨论了试探性定义,但在这种情况下它们是一个转移话题。试探性定义只涉及在一个翻译单元内拥有多个表面定义是合法的。问题的重点在于为什么C版本似乎会在单独的翻译单元中存在多个定义时进行链接。就我个人而言,我认为您应该更清楚地指出代码错误。 - CB Bailey
@Charles:在我看来,这并不是误导,也不是故意转移话题。我个人认为,全面了解情况非常值得。这也是我认为有些人可能无法理解你的回答的原因之一。它太简洁了。 - László Papp

5

gcc实现了一个传统的特性,即未初始化的全局变量被放置在一个公共块中。

尽管在每个翻译单元中定义是暂定的,在ISO C中,在翻译单元的末尾,如果暂定定义还没有合并到非暂定定义中,则将暂定定义“升级”为完整定义。

在标准C中,即使这些定义来自于暂定定义,在具有外部链接的相同变量在多个翻译单元中定义是不正确的。

要获得与C++相同的行为,您可以使用gcc的-fno-common开关,这将导致相同的错误。(如果您正在使用GNU链接器并且不使用-fno-common,您可能还需要考虑使用--warn-common/-Wl,--warn-common选项来突出显示遇到具有相同名称的多个公共和非公共符号时的链接时间行为。)

从gcc手册中:

-fno-common

在C代码中,控制未初始化的全局变量的放置位置。Unix C编译器传统上允许在不同的编译单元中对这种变量进行多次定义,方法是将变量放置在一个公共块中。这是由-fcommon指定的行为,并且是大多数目标上GCC的默认行为。另一方面,这种行为不是ISO C所要求的,在某些目标上可能会在变量引用时带来速度或代码大小的惩罚。-fno-common选项指定编译器应该将未初始化的全局变量放置在对象文件的数据部分中,而不是生成它们作为公共块。这意味着如果在两个不同的编译中声明了相同的变量(没有extern),则在链接它们时会出现多重定义错误。在这种情况下,您必须改为使用-fcommon进行编译。使用-fno-common对于提供更好性能的目标很有用,或者如果您希望验证程序将在始终以这种方式处理未初始化变量声明的其他系统上正常工作。

gcc的行为是常见的,并且在标准的附录J中描述了它(这并非规范),其中描述了标准的常见扩展:

J.5.11 多个外部定义

一个对象的标识符可能有多个外部定义,有或没有显式使用关键字extern; 如果定义不一致,或者有多个初始化,行为是未定义的(6.9.2)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接