c语言头文件中定义全局变量的问题 (转http://www.cnblogs.com/Sorean/)

先说一下,全局变量只能定义在 函数里面,任意函数,其他函数在使用的时候用extern声明。千万不要往头文件里面写定义(int aa)。

即使是写,也只写声明 即可 extern int aa。

问题是这么开始的:

最近在看一个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 

There may be more than one external definition for the identifier of an object, with or without the explicit use of the keyword extern; if the definitions disagree, or more than one is initialized, the behavior is undefined (6.9.2).

多么尴尬的一段话,我们可以理解为gcc 和 vc允许在整个程序编译过程中的“tentative definition”,而非单一个"translation unit"内。

那么我们便可以理解之前两个int a的不会报错的场景了。gcc vc 视这样的没有初始化的变量为extern而非define。

同样可以理解的是,如果我们添加了初始化值:

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

则会报错了:

➜  t  cc t1.cpp
t1.cpp:5:5: error: redefinition of 'a'
int a;
^
./t1.h:4:5: note: previous definition is here
int a ;
^
t1.cpp:6:5: error: redefinition of 'a'
int a;
^
./t1.h:4:5: note: previous definition is here
int a ;
^
2 errors generated.

结合tentative definition的定义,便不难理解了。

到这里,细心的读者可能发现,我们这里的tentative definition只局限于C语言,是的。C++并不认可这一概念,把所有的int a; 视为变量定义。所以,如果使用c++,这些又会全部变成 redefinition 或者 duplicate symbol了。

吐槽:一个看似简单的问题,查阅了一天的资料,引申出这么多概念,才彻底弄明白,我真的学过C嘛( ⊙ o ⊙ )?

c语言头文件中定义全局变量的问题的更多相关文章

  1. C语言头文件中定义全局变量导致重复定义错误

    合作方升级SDK后,程序编译出现变量重复定义的错误,通过错误提示无法找到什么位置重复定义了,但确定是引入新SDK后才出现的错误,从SDK的头文件中查找,最终发现在头文件中定义了全局变量 我们的项目在多 ...

  2. C语言之在头文件中定义全局变量

    通常情况下,都是在C文件中定义全局变量,在头文件中声明,但是,如果我们定义的全局变量需要被很多的C文件使用的话,那么将全局变量定义在头文件里面会方便很多,那到底是如何实现的? os_var.c文件内容 ...

  3. [Linux][C][gcc][tips] 在头文件中定义变量引发的讨论

    概述 本人的原创文章,最先发表在github-Dramalife-note中.转载请注明出处. Define variable(s) in header file referenced by mult ...

  4. C++ vector 实现二维数组时, 在类的头文件中定义时遇到"应输入类型符"的问题?

    见下,当我在类的声明文件中定义二维vector时,提示我应输入类型说明符; 但是相同的格式定义,在类中将二维vector修改为在源文件中定义就可以顺利通过,并顺利执行打印 打印结果如下: 望大神来解惑 ...

  5. 在C的头文件中定义的结构体,如何在cpp文件中引用

    解决方案1:在cpp文件中放置.c,且在该文件中引用变量 解决方案2:在一个cpp文件中包含.c,但在另一个cpp文件中使用结构体变量 cpp文件1 cpp文件2 #include "dia ...

  6. 嵌入式C语言头文件的建立与使用

    如何正确编写 C 语言头文件和与之相关联的 c 源程序文件,这首先就要了解它们的各自功能. 要理解 C 文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程. 一般说来编译器会做以下几 ...

  7. C语言头文件到底是什么?

    C语言头文件到底是什么? 在C语言学习的时候总是会引入这样的语句#include <stdio.h>,书上解释说把stdio.h这个文件的全部内容直接插入到这个位置,然后再经过C语言的编译 ...

  8. C语言中定义全局变量

    (1)在C语言的头文件中定义变量出现的问题 最好不要傻嘻嘻的在头文件里定义什么东西.比如全局变量: /*xx头文件*/ #ifndef  _XX_头文件.H #define  _XX_头文件.H in ...

  9. [C/C++]在头文件中使用static定义变量意味着什么

    文章出处:http://www.cnblogs.com/zplutor/ 看到有一位同学在头文件中这么写: static const wchar_t* g_str1 = - static const ...

随机推荐

  1. javascript遍历数组最优写法

    var arr = [];//这样定义的数组,是null,等待开辟空间 var arr = new Array();//不建议使用,会占用一块内存空间 var i=0,len=arr.length; ...

  2. Expect 初学

    expect可以帮助脚本完成自动化.今天就用二种实例来介绍2种写法. 安装 yum -y install expect 一.直接用/usr/bin/expect 这种就不方便调用linux下的环境变量 ...

  3. IOS第18天(10,核心动画-转盘,自定义buton,旋转动画)

    *****HMViewController.m #import "HMViewController.h" #import "HMWheelView.h" @in ...

  4. 多个git账号的配置

    问题描述: 作为开发人员,普遍有多个git账号,例如,公司邮箱对应的公司仓储账号和私人邮箱对应的github账号.在一台电脑上使用两个账号基于ssh协议拉代码,如果不进行额外设置,往往只有一个账号可以 ...

  5. 前端文摘:Web 开发模式演变历史和趋势

    今天的<前端文摘>给大家分享一篇玉伯的文章.文章详细介绍了 Web 开发的四种常用模式以及未来可能成为流行趋势的 Node 全栈开发模式,相信你看了以后一定会有收获. 您可能感兴趣的相关文 ...

  6. PHP->利用GD库新建图像

    1.确认php中GD库是否开启 在PHP配置文件php.ini中查找extension=php_gd2.dll,去掉前边的(分号) ';' 即可,一般php是默认开启的 2.绘画步骤 创建一个画布(画 ...

  7. SHARE NOTHING SHARE EVERYTHING

    http://mp.weixin.qq.com/s?__biz=MjM5ODYwMjI2MA==&mid=2649736156&idx=1&sn=23931f48282f6ef ...

  8. Java学习-043-获取文件在目录中的路径

    我们在日常的电脑使用中,经常需要在当前目录或当期目录及其子目录中查找文件,并获取相应的文件路径名.在我们的自动化测试中,也经常需要确认文件在目录中是否成功生成或已存在,因而我写了一个小方法来实现. 获 ...

  9. Java反射的三种实现方式

    Foo foo = new Foo(); 第一种:通过Object类的getClass方法 Class cla = foo.getClass(); 第二种:通过对象实例方法获取对象 Class cla ...

  10. LeetCode Strobogrammatic Number

    原题链接在这里:https://leetcode.com/problems/strobogrammatic-number/ 题目: A strobogrammatic number is a numb ...