15

我正在阅读这里的代码(中文)。有一段代码是关于在 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了变化。我无法理解它是如何工作的!

4

6 回答 6

19

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

外部定义是一个外部声明,它也是函数(内联定义除外)或对象的定义。如果用外部链接声明的标识符在表达式中使用(而不是作为sizeofor_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 编译器中抑制“通用模型”。当我在我的系统上对此进行测试时,它导致类似于您的代码的“严格参考/定义模型”行为:

$ 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
$

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


参考资料:
很遗憾,我不能给你我的 C11 标准副本的链接C 中的变量是
什么?extern
“链接器初学者指南”
关于外部变量模型的 SAS 文档

于 2013-07-23T02:05:30.680 回答
3

形式上,多次定义具有外部链接的相同变量(或函数)是非法的。因此,从形式上看,您的程序的行为是未定义的。

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

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

于 2013-07-23T02:48:21.537 回答
2

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

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

在 th:

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;
于 2013-07-23T01:59:46.970 回答
1

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

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

于 2013-07-23T01:57:16.030 回答
0

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

于 2013-07-23T01:56:55.357 回答
-1

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

于 2013-07-23T01:56:55.903 回答