【问题标题】:C the same global variable defined in different filesC 同一个全局变量定义在不同的文件中
【发布时间】:2013-07-21 22:22:42
【问题描述】:

我正在阅读this code from here(中文)。有一段代码是关于在 C 中测试全局变量的。变量a 已在文件t.h 中定义,该文件已包含两次。在文件foo.c 中定义了一个带有一些值的struct b 和一个main 函数。在main.c 文件中,定义了两个未初始化的变量。

/* t.h */
#ifndef _H_
#define _H_
int a;
#endif

/* foo.c */
#include <stdio.h>
#include "t.h"

struct {
   char a;
   int b;
} b = { 2, 4 };

int main();

void foo()
{
    printf("foo:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
        \tsizeof(b)=%d\n\tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
        &a, &b, sizeof b, b.a, b.b, main);
}

/* main.c */
#include <stdio.h>
#include "t.h"

int b;
int c;

int main()
{
    foo();
    printf("main:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
        \t(&c)=0x%08x\n\tsize(b)=%d\n\tb=%d\n\tc=%d\n",
        &a, &b, &c, sizeof b, b, c);
    return 0;
}

使用Ubuntu GCC 4.4.3编译后,结果如下:

foo:    (&a)=0x0804a024
    (&b)=0x0804a014
    sizeof(b)=8
    b.a=2
    b.b=4
    main:0x080483e4
main:   (&a)=0x0804a024
    (&b)=0x0804a014
    (&c)=0x0804a028
    size(b)=4
    b=2
    c=0

变量ab在两个函数中地址相同,但b的大小发生了变化。我无法理解它是如何工作的!

【问题讨论】:

  • 你有什么问题?
  • 使用%p 打印指针,你也应该在标题中添加foo
  • 如果要解决冲突,请声明 var static。
  • 来自coolshell(中文)对吧?
  • @liuan 不,我只是好奇,仅此而已。我认为这不是问题,因为您没有复制整篇文章。就我个人而言,我也不喜欢那篇文章中的解释。所以我很高兴看到这里讨论的问题。

标签: c linker one-definition-rule


【解决方案1】:

在编译 foo 时,范围内的 b 是两个整数向量 {2, 4} 或当 sizeof(int) 为 4 时为 8 个字节。 编译 main 时, b 刚刚被重新声明为 int,因此大小为 4 是有意义的。此外,在“a”之后可能会在结构中添加“填充字节”,以便下一个插槽(int)在 4 字节边界上对齐。

【讨论】:

    【解决方案2】:

    a 和 b 具有相同的地址,因为它们出现在文件中的相同点。 b 是不同大小的事实与变量从哪里开始无关。如果您在其中一个文件的 a 和 b 之间添加了变量 c,则 bs 的地址会有所不同。

    【讨论】:

      【解决方案3】:

      b 具有相同的地址,因为 链接器 决定为您解决冲突。

      sizeof 显示不同的值,因为 sizeof 在编译时进行评估。在这个阶段,编译器只知道一个b(当前文件中定义的那个)。

      【讨论】:

      • +1 表示知道是链接器将bs 放在同一内存位置。
      【解决方案4】:

      这段代码似乎故意打破了单一定义规则。它会调用未定义的行为,不要那样做。

      关于全局变量a:不要把全局变量的定义放在头文件中,因为它会包含在多个.c文件中,导致多重定义。只需将声明放在标题中并将定义放在 .c 文件之一中。

      在 t.h:

      extern int a;
      

      在 foo.c 中

      int a;
      

      关于全局变量b:不要多次定义,用static限制文件中的变量。

      在 foo.c 中:

      static struct {
         char a;
         int b;
      } b = { 2, 4 };
      

      在 main.c 中

      static int b;
      

      【讨论】:

      • +1 用于特定补救措施以避免违反一项定义规则。
      【解决方案5】:

      您违反了 C 的“单一定义规则”,结果是未定义的行为。 “单一定义规则”并未在标准中正式声明。我们正在查看不同源文件(又名翻译单元)中的对象,因此我们关注“外部定义”。阐明了“一个外部定义”语义(C11 6.9 p5):

      外部定义是一个外部声明,它也是函数(内联定义除外)或对象的定义。如果在表达式中使用了用外部链接声明的标识符(而不是作为结果为整数常量的sizeof_Alignof 运算符的操作数的一部分),则在整个程序的某处应该有标识符的一个外部定义;否则,不得超过一个。

      这基本上意味着您最多只能定义一个对象一次。 (如果外部对象从未在程序中的任何地方使用,则 else 子句允许您根本不定义外部对象。)

      请注意,b 有两个外部定义。一个是你在foo.c中初始化的结构,另一个是main.c中的暂定定义,(C11 6.9.2 p1-2):

      如果对象标识符的声明具有文件范围和初始化程序,则 声明是标识符的外部定义。

      具有文件范围的对象的标识符声明没有初始化程序,没有存储类说明符或存储类说明符static,构成一个暂定定义。如果翻译单元包含一个或多个标识符的暂定定义,并且翻译单元不包含该标识符的外部定义,则行为与翻译单元包含该标识符的文件范围声明完全相同,复合类型为翻译单元的末尾,初始化器等于 0。

      所以你有多个b 的定义。但是,还有另一个错误,因为您使用不同的类型定义了 b。首先请注意,允许对具有外部链接的同一对象进行多个声明。但是,当在两个不同的源文件中使用相同的名称时,该名称指的是同一个对象(C11 6.2.2 p2):

      在构成整个程序的一组翻译单元和库中,每个 具有外部链接的特定标识符的声明表示相同的对象或 功能。

      C 严格限制对同一对象的声明 (C11 6.2.7 p2):

      引用同一对象或函数的所有声明都应具有兼容的类型; 否则,行为未定义。

      由于每个源文件中b 的类型实际上并不匹配,因此行为未定义。 (什么构成兼容类型在所有 C11 6.2.7 中都有详细描述,但基本上归结为类型必须匹配。)

      所以b 有两个失败之处:

      • 多个定义。
      • 具有不兼容类型的多个声明。

      从技术上讲,您在两个源文件中声明 int a 也违反了“一个定义规则”。注意a 有外部链接(C11 6.2.2 p5):

      如果对象标识符的声明具有文件范围且没有存储类说明符,则其链接是外部的。

      但是,从前面 C11 6.9.2 的引用来看,那些 int a 暂定定义是外部定义,您只能在顶部引用 C11 6.9 中的其中一个。

      通常的免责声明适用于未定义的行为。任何事情都可能发生,这将包括您观察到的行为。


      C 的一个常见扩展是允许多个外部定义,并在信息性附录 J.5 (C11 J.5.11) 中的 C 标准中进行了描述:

      一个对象的标识符可能有多个外部定义,带有或 没有明确使用关键字extern如果定义不一致,或不止一个被初始化,行为未定义 (6.9.2)。

      (重点是我的。)因为a 的定义一致,所以没有什么坏处,但b 的定义不一致。这个扩展解释了为什么你的编译器不会抱怨存在多个定义。根据 C11 6.2.2 的引用,链接器将尝试协调对同一对象的多个引用。

      链接器通常使用两种模型之一来协调多个翻译单元中同一符号的多个定义。这些是“通用模型”和“参考/定义模型”。在“通用模型”中,多个同名对象以union样式的方式折叠成一个对象,使对象呈现最大定义的大小。在“Ref/Def 模型”中,每个外部名称必须只有一个定义。

      GNU 工具链默认使用“通用模型”和“宽松的 Ref/Def 模型”,它对单个翻译单元执行严格的定义规则,但不会抱怨跨多个翻译单元的违规行为。

      可以使用-fno-common 选项在 GNU 编译器中抑制“通用模型”。当我在我的系统上对此进行测试时,它会导致类似于您的代码的“严格 Ref/Def 模型”行为:

      $ cat a.c
      #include <stdio.h>
      int a;
      struct { char a; int b; } b = { 2, 4 };
      void foo () { printf("%zu\n", sizeof(b)); }
      $ cat b.c
      #include <stdio.h>
      extern void foo();
      int a, b;
      int main () { printf("%zu\n", sizeof(b)); foo(); }
      $ gcc -fno-common a.c b.c
      /tmp/ccd4fSOL.o:(.bss+0x0): multiple definition of `a'
      /tmp/ccMoQ72v.o:(.bss+0x0): first defined here
      /tmp/ccd4fSOL.o:(.bss+0x4): multiple definition of `b'
      /tmp/ccMoQ72v.o:(.data+0x0): first defined here
      /usr/bin/ld: Warning: size of symbol `b' changed from 8 in /tmp/ccMoQ72v.o to 4 in /tmp/ccd4fSOL.o
      collect2: ld returned 1 exit status
      $
      

      我个人认为,无论多对象定义的解析模型如何,都应该始终提供链接器发出的最后一个警告,但这既不是这里也不是那里。


      参考资料:
      Unfortunately, I can't give you the link to my copy of the C11 Standard
      What are extern variables in C?
      The "Beginner's Guide to Linkers"
      SAS Documentation on External Variable Models

      【讨论】:

      • 值得注意的是,两个典型的实现是通常称为“def/ref 模型”和“普通模型”的实现。后者是典型的 Unix/Linux 链接器使用的,并导致观察到的行为。短语“通用模型”似乎指的是 1960 年代在许多大型机上实现的 Fortran COMMON 块。
      • @torek:感谢您促使我做更多的研究。我已经更新了答案。
      • 此答案引用自 C 2011 J.5.11 的文本描述了 C 语言的常见扩展。它不是标准的确定部分。 C 标准中暗示单一定义规则的部分既不是 J.5.11、6.9.2 也不是 6.2.2。它是 6.2.7 2:“引用相同对象或函数的所有声明应具有兼容的类型;否则,行为未定义。”
      • @EricPostpischil:谢谢!我重新阅读了 6.9,我不敢相信我之前错过了它。它几乎说“一个外部定义”。我已经更新了答案。
      【解决方案6】:

      形式上,多次定义具有外部链接的同一个变量(或函数)是非法的。所以,从形式上看,你的程序的行为是不确定的。

      实际上,允许使用外部链接对同一个变量进行多个定义是一种流行的编译器扩展(一种常见的扩展,在语言规范中如此提及)。但是,为了正确使用,每个定义都应声明为相同的类型。并且包含初始化器的定义不超过一个。

      您的案例与常见的扩展描述不符。您的代码编译为该通用扩展的副作用,但其行为仍未定义。

      【讨论】:

      • 我之前已经对这个答案投了赞成票,但这个答案首先专门针对允许多个外部定义的通用扩展。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-08-21
      • 2015-03-18
      • 2021-02-24
      • 2011-11-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多