【问题标题】:Tentative definitions in C and linkingC 中的暂定定义和链接
【发布时间】:2010-12-02 05:15:42
【问题描述】:

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

f1.c:

int x;

f2.c:

int x=2;

我对@9​​87654321@ 的第6.9.2 段的阅读是这个程序应该被拒绝。在我对 6.9.2 的解释中,变量 x 暂定在 f1.c 中定义,但这个暂定定义成为翻译单元末尾的实际定义,因此(在我看来)应该表现得好像 @987654326 @ 包含定义 int x=0;

对于我能够尝试的所有编译器(以及重要的链接器),这不是发生的情况。我尝试的所有编译平台都链接了上述两个文件,两个文件中x的值都是2。

我怀疑这是偶然发生的,或者只是作为标准要求之外的“简单”功能。如果您考虑一下,这意味着链接器对那些没有初始化程序的全局变量有特殊的支持,而不是那些显式初始化为零的变量。有人告诉我,无论如何编译 Fortran 可能都需要链接器功能。这将是一个合理的解释。

对此有什么想法吗?标准的其他解释?文件f1.cf2.c 拒绝链接在一起的平台名称?

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

【问题讨论】:

  • 感谢分享。学习永远不会太老
  • 只有当你违反约束段落中的内容时,编译器才需要拒绝(即警告或错误)。您的事物可能没有两个外部定义的约束是“应”约束段落之外。违反约束之外的任何 shall 会自动导致 C 中未定义的行为 - 这就是允许编译器按照它想要的方式对待它的原因。
  • @litb 这是一个有趣的观点。我提到的静态分析器尽可能不标记/已建立/编程实践,即使它们没有被标准定义。在这里,我认为我们将决定不发出警告,因为在不支持这些多个定义的平台上,可能它们会导致链接时失败,而不是运行时失败。 PS:我知道“未定义”是什么意思,但是每个额外的分析选项都会使分析器的可用性降低一些,并且必须权衡收益。因此,问题的“平台名称......”部分
  • 最近的 gcc 版本默认使用-fno-common。然后,即使您在 f2.c 中只有 int x; 而没有初始化,您也会收到链接器错误。恕我直言,跨编译单元合并暂定定义是不好的。它会导致错误。现在存在 extern 关键字以正确执行操作。

标签: c compilation fortran static-analysis c99


【解决方案1】:

另见What are extern variables in C。这在信息性附录 J 中的 C 标准中被提及为通用扩展:

J.5.11 多个外部定义

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

警告

正如@litb 在这里指出的那样,正如我在对交叉引用问题的回答中所述,对全局变量使用多个定义会导致未定义的行为,这是标准的说法“任何事情都可能发生”。可能发生的事情之一是程序的行为符合您的预期; J.5.11 大约说,“你可能比你应得的更幸运”。但是,一个依赖于 extern 变量的多个定义的程序——无论有没有显式的“extern”关键字——并不是一个严格遵守的程序,也不能保证在任何地方都能工作。等效地:它包含一个 bug,它可能会显示也可能不会显示。

【讨论】:

  • 当我专门询问非外部变量时,本段中确实有一个有趣的说明。感谢您的参考。这就是我喜欢标准的原因......
  • 由于这两个变量都在文件范围内并且不是静态的(它们必须像那样才能有任何问题),它们都是“外部”——有或没有显式使用关键字 extern。
  • 要明确是否允许:不,这是 C 中未定义的行为。即使 aint a[1];,这就像做 a[10] = 0; 一样,这是并且被允许作为常见的扩展(在我们拥有灵活的数组成员之前)。我认为应该清楚地指出,除了在某些平台上定义了行为之外,正式地这样做是未定义的行为。
  • @Jonathan,如果我对我的 UB cmets 有点恼火,我很抱歉 :) 我只是认为提问者可能会认为 C 标准以某种方式允许程序执行“通用扩展名”这并保持严格符合:) 当然 +1 了你
  • 请注意,extern 关键字使它成为一个声明,而不是一个定义,所以没有办法拥有“带有显式 extern 关键字的多个定义”
【解决方案2】:

标准中有一种称为“通用扩展”的东西,只要变量只初始化一次,就可以多次定义变量。见http://c-faq.com/decl/decldef.html

链接页面说这与 Unix 平台有关——我猜 c99 和 c89 相同——尽管它可能已被更多编译器采用以形成某种事实上的标准。很有趣。

【讨论】:

    【解决方案3】:

    这是为了澄清我对 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
    

    【讨论】:

    • 如果有人不熟悉 nm 的输出:定义了 D。 U 未定义。而来自 man nm - "C" 的符号很常见。常用符号是未初始化的数据。链接时,可能会出现多个同名的常用符号。如果符号在任何地方定义,公共符号将被视为未定义的引用。
    • 感谢帕斯卡的澄清。
    猜你喜欢
    • 1970-01-01
    • 2011-09-07
    • 2014-02-13
    • 1970-01-01
    • 2011-04-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-13
    相关资源
    最近更新 更多