C语言头文件中定义全局变量的问题

C语言头文件中定义全局变量的问题

问题是这么开始的:

最近在看一个PHP的扩展源码,编译的时候的遇到一个问题:

ld: 1 duplicate symbol for architecture x86_64

仔细看了一下源码,发现在头文件中 出现了全局变量的定义。

简化一下后,可以这么理解:

// t1.h
#ifndef T1_H
#define T1_H

int a = 0;

#endif
//------------------

//t1.c
#include "t1.h"
#include "t2.h"

int main(){
    return 0;
}
//-----------------

//t2.h
#include "t1.h"
//empty
//----------------

//t2.c
#include "t2.h"
//empty
//-------

这两个c文件能否通过编译?想必有点经验的必会说 不会,重定义了。

那么是否真的如此?并不这么简单。


•第一个问题,#ifndef 的这个宏是否防止了重定义(redefinition)?

答案:是。但是是在单个translation unit中(wiki translation unit)。

#ifndef 的头文件宏称为 include guards的(wiki)

我们知道,一个完整的编译的过程是经过

one.c  -->  PREPROCESSOR ->  tmp.c(temporary)  ->  COMPILER  ->  one.obj  -> LINKER ->  one.exe

这三个过程的,而在预编译阶段,便会把include的文件展开,我们使用cc -E 命令来查看t1.c的预编译的结果:

➜  t  cc -E t1.c     
# 1 "t1.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 321 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "t1.c" 2

# 1 "./t1.h" 1

int a = 0;
# 3 "t1.c" 2
# 1 "./t2.h" 1
# 4 "t1.c" 2

int main(void){
 return 0;
}

看到编译器把 t1.h 做了展开,我们看到了 a的定义。

而在t2.c 的预编译结果里,我们同样看到了a的展开定义:

➜  t  cc -E t2.c 
# 1 "t2.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 321 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "t2.c" 2
# 1 "./t2.h" 1
# 1 "./t1.h" 1

int a = 0;
# 2 "./t2.h" 2
# 2 "t2.c" 2

所以到了Link阶段,编译器会看见两个a的定义。原因在于 include guards 只在同一个translation unit(一个c文件和include的文件的编译过程)内起作用,两个编译单元是编译过程是分开的,所以无法察觉到另外一个里面的#ifdefine内容,可以这么理解:

t1.c -> t1.s -> t2.o
                          \
                            *-> - t.otu
                          /
t2.c -> t2.s -> t2.o

所以,在头文件中是不应该define 变量,只应该declare。

include guards 是为了防止两个文件相互引用而造成的循环引用问题。读者可以试试去除include guards,看看效果。

以上的解答也同时解释了 为什么 include guards 没有在这个例子下起到防止重定义的作用。

那么,如何强制在头文件中定义全局变量呢?

正确的做法是头文件declare,c文件define,老生常谈的问题,不再赘述。这里提供两个技巧:对于函数,有人给出这么个办法,添加inline或者static 关键字。

或者有人直接这么搞:

#ifdef DEFINE_GLOBALS
#define EXTERN
#else
#define EXTERN extern
#endif
EXTERN int global1;
EXTERN int global2;

那么在头文件中定义全局变量真的一定是错误的吗?

答案是不一定。

如果我们写这样一个c文件:

int a;
int a;
int main(void){
    return 0;
}

你肯定认为是重定义了,不过你可以试试 cc ,并不会报错,甚至没有warning。

原因其实在于 tentative defination,C99里的相关定义是

A declaration of an identifier for an object that has file scope without an initializer, and without a storage-class specifier or with the storage-class specifier static, constitutes a tentative definition.If a translation unit contains one or more tentative definitions for an identifier, and the translation unit contains no external definition for that identifier, then the behavior is exactly as if the translation unit contains a file scope declaration of that identifier, with the composite type as of the end of the translation unit, with an initializer equal to 0.

意义是,如果declare了一个变量,但是没有初始化,在同一个translation unit结束后,还没有发现初始化,那么应该把这个变量赋值为0。所以,如果依据C99的规则,你在头文件中写入

// t1.h
 
int a;

仍然会被编译为int a = 0。所以多次包含,仍然会重定义报错。

而gcc vc并没有完全遵循这个标准,C99中最后面还有一段:

Multiple external definitions

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/9a38a4b85f4106cbf2ca9298c2e059a9.html