C在不同文件中定义的相同全局变量

我正在这里阅读这段代码 (中文)。 有一段关于在C中测试全局变量的代码。变量a已经在文件中定义了两次。 在文件foo.c定义了一个带有一些值和一个main函数的struct b 。 在main.c文件中,定义了两个没有初始化的变量。

 /* th */ #ifndef _H_ #define _H_ int a; #endif /* foo.c */ #include  #include "th" 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, ba, bb, main); } /* main.c */ #include  #include "th" 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 ba=2 bb=4 main:0x080483e4 main: (&a)=0x0804a024 (&b)=0x0804a014 (&c)=0x0804a028 size(b)=4 b=2 c=0 

变量ab在两个函数中具有相同的地址,但是b的大小已经改变。 我无法理解它是如何工作的!

您违反了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):

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

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

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

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

所以你有两个b失败:

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

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

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

但是,从前面C11 6.9.2的引用来看,暂定定义中的那些是外部定义,并且只允许其中一个来自顶部的C11 6.9的引用。

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


C的一个共同扩展是允许多个外部定义,并在资料性附件J.5(C11 J.5.11)的C标准中进行了描述:

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

(重点是我的。)由于同意的定义,没有任何伤害,但b的定义不一致。 此扩展解释了为什么您的编译器不会抱怨存在多个定义。 根据C11 6.2.2的引用,链接器将尝试协调对同一对象的多个引用。

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

GNU工具链默认使用“Common Model”和“Relaxed Ref / Def Model”,它对单个翻译单元强制执行严格的一个定义规则,但不会抱怨跨多个翻译单元的违规行为。

可以使用-fno-common选项在GNU编译器中抑制“Common Model”。 当我在我的系统上测试它时,它会导致类似于你的代码的“严格参考/默认模型”行为:

 $ cat ac #include  int a; struct { char a; int b; } b = { 2, 4 }; void foo () { printf("%zu\n", sizeof(b)); } $ cat bc #include  extern void foo(); int a, b; int main () { printf("%zu\n", sizeof(b)); foo(); } $ gcc -fno-common ac bc /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文档

在forms上,使用外部链接多次定义相同的变量(或函数)是非法的。 因此,从正式的角度来看,程序的行为是不确定的。

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

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

这段代码似乎打算打破单一定义规则。 它将调用未定义的行为,不要这样做。

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

在:

 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; 

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

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

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

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