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

liu*_*uan 15 c linker one-definition-rule

我正在这里阅读这段代码(中文).有一段关于在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;
}
Run Code Online (Sandbox Code Playgroud)

使用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
Run Code Online (Sandbox Code Playgroud)

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

jxh*_*jxh 19

您违反了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 Model"中,每个外部名称必须只有一个定义.

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

可以使用该-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
$
Run Code Online (Sandbox Code Playgroud)

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


参考文献:
不幸的是,我无法给你链接到我的C11标准副本C中有
哪些extern变量?关于外部变量模型
的"链接器初学者指南"
SAS文档