我正在阅读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
变量a
和b
在两个函数中具有相同的地址,但b
的大小已更改。我无法理解它是如何运作的!
答案 0 :(得分:19)
您违反了C&#34;一个定义规则&#34;,结果是未定义的行为。 &#34;一个定义规则&#34;在标准中没有正式声明。我们正在查看不同源文件(也就是翻译单元)中的对象,因此我们关注&#34;外部定义&#34;。 &#34;一个外部定义&#34;语义拼写出来(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
也违反了&#34;一个定义规则&#34;。请注意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的引用,链接器将尝试协调对同一对象的多个引用。
链接器通常使用两个模型中的一个来协调多个转换单元中相同符号的多个定义。这些是#34; Common Model&#34;和#34; Ref / Def模型&#34;。在&#34; Common Model&#34;中,具有相同名称的多个对象以union
样式方式折叠到单个对象中,以使对象具有最大定义的大小。在&#34; Ref / Def Model&#34;中,每个外部名称必须只有一个定义。
GNU工具链使用&#34; Common Model&#34;默认情况下,还有一个&#34;轻松参考/默认模型&#34;,它对单个翻译单元强制执行严格的一个定义规则,但不会抱怨多个翻译单元的违规行为。
&#34; Common Model&#34;可以使用-fno-common
选项在GNU编译器中抑制。当我在我的系统上测试它时,它导致&#34; Strict Ref / Def Model&#34;与您的代码类似的行为:
$ 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
答案 1 :(得分:3)
正式地,使用外部链接多次定义相同的变量(或函数)是非法的。因此,从正式的角度来看,程序的行为是不确定的。
实际上,允许使用外部链接对同一变量进行多个定义是一种流行的编译器扩展(一种常见的扩展,在语言规范中提到)。但是,为了正确使用,每个定义应使用相同的类型声明它。并且只有一个定义应包括初始化器。
您的案例与常见的扩展程序说明不符。您的代码编译为该公共扩展的副作用,但其行为仍未定义。
答案 2 :(得分:2)
这段代码似乎打算打破单一定义规则。它将调用未定义的行为,不要这样做。
关于全局变量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;
答案 3 :(得分:1)
b
具有相同的地址,因为链接器决定为您解决冲突。
sizeof
显示不同的值,因为sizeof在编译时进行评估。在此阶段,编译器只知道一个b
(在当前文件中定义的那个)。
答案 4 :(得分:0)
在编译foo时,范围内的b
是两个整数向量{2, 4}
或当sizeof(int)为4时为8个字节。
当编译main时,b刚刚被重新声明为int
,因此大小为4是有意义的。也可能在“a”之后向结构中添加“填充字节”,使得下一个槽(int)在4个字节边界上对齐。
答案 5 :(得分:-1)
a和b具有相同的地址,因为它们出现在文件中的相同位置。 b是不同大小的事实与变量开始的位置无关。如果在其中一个文件中的a和b之间添加了变量c,则bs的地址会有所不同。