C语言中的临时定义和链接

41

考虑由两个文件组成的C程序,

f1.c:

int x;

f2.c:

int x=2;

我的阅读理解是,在C99标准的第6.9.2段中,这个程序应该被拒绝。在我对6.9.2的解释中,变量xf1.c中被暂时定义,但这个暂定定义会在翻译单元的结尾变成实际定义,并且(在我看来)应该像f1.c包含定义int x=0;一样运行。

在我试过的所有编译器(和链接器)中,都没有发生这种情况。我尝试过的所有编译平台都会链接上述两个文件,并且x的值在两个文件中都为2。

我怀疑这不是偶然发生的,也不仅仅是作为标准要求之外提供的“简单”功能。如果你仔细想一下,这意味着链接器对那些没有初始化器的全局变量有特殊支持,而不是那些明确初始化为零的变量。有人告诉我,链接器的这个特性可能在编译Fortran时是必需的。这将是一个合理的解释。

对此有什么想法?标准的其他解释?文件f1.cf2.c不能在一些平台上链接的名称?

注意:这很重要,因为这个问题发生在静态分析的背景下。如果这两个文件在某些平台上可能拒绝链接,分析器应该报错,但如果每个编译平台都接受它,则没有理由警告。


4
感谢分享。学习永远不会太晚。 - Adriaan
4
仅当您违反限制段落中的规定时,编译器才需要拒绝(即警告或错误)操作。对于您的事物,不得有两个外部定义的限制是在一个约束段落之外的“必须”。在C语言中,违反任何一个“必须”都会自动导致未定义行为 - 这使得编译器可以根据自己的意愿处理它。 - Johannes Schaub - litb
@litb 这是一个有趣的观点。我提到的静态分析器会尽可能避免标记/已建立的/编程实践,即使它们没有被标准定义。在这种情况下,我认为我们将决定不发出警告,因为在这些多个定义不受支持的平台上,它们可能会导致链接时间失败,而不是运行时故障。 PS:我知道“未定义”的含义,但每个额外的分析选项都会使分析器的可用性略微降低,必须权衡其利弊。因此,问题的“在哪些平台上...”部分需要考虑。 - Pascal Cuoq
2
近期的gcc版本默认使用-fno-common。即使在f2.c中只有未初始化的int x;,你也会得到链接器错误。跨编译单元合并试探性定义是不好的,我认为这会导致错误。现在存在extern关键字来正确处理这些事情。 - Sven
3个回答

35

另请参见C语言中的extern变量是什么。 这在C标准的信息附录J中被提及为常见扩展:

J.5.11 多个外部定义

可以有一个或多个对象标识符的外部定义,无论是否显式使用关键字extern;如果这些定义不一致或者其中有一个以上被初始化,则其行为是未定义的(6.9.2)。

警告

如@litb在此处指出,并如我回答的相关问题中所述,对于全局变量使用多个定义会导致未定义的行为,这是标准表达“任何事情都可能发生”的方式。可能发生的一件事是程序的表现与您期望的相同;而J.5.11则表示,“您可能比应得的更幸运”。但一个依赖于多个extern变量定义的程序 - 无论是否显式使用关键字 extern - 不是一个严格符合标准的程序,不能保证在任何地方都能工作。同样地:它包含一个可能会或可能不会显示的错误

另请参见如何使用 extern 在源文件之间共享变量?

正如Sven评论中所指出的,在我回答“如何使用extern”问题时,GCC最近相对较新的默认规则已经发生了变化。在GCC 10.x(从2020年5月开始)和以后版本中,默认的编译模式使用-fno-common,而在之前的版本中,默认模式使用-fcommon。新行为意味着您不能再使用多个试探性的定义,而这正是C标准要求严格遵守的。

如果您使用GCC,并且有滥用多个试探性定义的代码,则可以在编译过程中添加-fcommon,并且它将像以前一样工作。然而,您的代码不是最大限度地可移植的,长期来看,更好的方法是修改代码,使每个变量在一个源文件中得到正确定义(与需要使用该变量的所有程序链接),并在一个头文件中得到正确声明(使用该变量的源文件都可以包含该头文件,定义该变量的源文件也应包括该头文件以确保一致性)。


4
由于这两个变量都在文件作用域中且不是静态的(它们必须是这样才会有任何问题),因此它们都是“extern”——无论是否显式使用关键字“extern”。 - Jonathan Leffler
2
为了确保清晰,是否允许这样做:在 C 语言中,不允许这样做,这是未定义的行为。这就像执行 a[10] = 0;,即使 a 是一个 int a[1];,在我们拥有灵活数组成员之前和之后都被允许作为常见扩展。我认为应该明确指出,在正式上,这样做是未定义的行为,除了在某些平台上具有定义的行为。 - Johannes Schaub - litb
1
@Jonathan,如果我的UB评论有点烦人,我很抱歉:) 我只是觉得问问题的人可能会认为通过听到“常见扩展名”,C标准以某种方式允许程序这样做并保持严格的符合性:) 当然我给你点赞。 - Johannes Schaub - litb
1
请注意,extern关键字使其成为声明而不是定义,因此没有办法使用显式的extern关键字来实现“多个定义”。 - Chris Dodd
@JohannesSchaub-litb: +1 我认为提问者可能会认为通过听到“常见扩展名”,C标准以某种方式允许程序执行此操作并保持严格符合性。- 这个 应该澄清什么是“常见扩展名”并缓解您的担忧。 - legends2k
显示剩余9条评论

15

有一种被称为“常用扩展”的标准,允许多次定义变量,只要变量仅在初始化时定义一次即可。参见http://c-faq.com/decl/decldef.html

链接的页面指出这对Unix平台很重要--我猜它对c99和c89也是相同的--虽然可能已经被更多编译器采纳形成某种事实标准。有趣。


7
这是为了澄清我对olovb评论的回答:
从“int x;”编译的目标文件进行nm输出。在该平台上,符号前缀为“_”,即变量x显示为_x。
00000000 T _main
         U _unknown
00000004 C _x
         U dyld_stub_binding_helper

对从“int x = 1;”编译的目标文件运行nm命令的输出结果

00000000 T _main
         U _unknown
000000a0 D _x
         U dyld_stub_binding_helper

以下是从“int x = 0;”编译的目标文件的nm输出:

00000000 T _main
         U _unknown
000000a0 D _x
         U dyld_stub_binding_helper

以下是从 "extern int x;" 编译的目标文件使用 nm 命令的输出结果

00000000 T _main
         U _unknown
         U dyld_stub_binding_helper

编辑:从“extern int x;”编译的目标文件的nm输出,其中x实际上在一个函数中使用
00000000 T _main
         U _unknown
         U _x
         U dyld_stub_binding_helper

6
如果有人不熟悉"nm"的输出:D表示已定义,U表示未定义;从"nm"的手册中得知,符号"C"代表常规。常规符号是未初始化数据。在链接时,可能会出现多个名称相同的常规符号。如果该符号在任何地方被定义,则常规符号将被视为未定义引用。 - Falaina

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