C语言陷阱与技巧第36节,#include包含C语言中的头文件是什么意思?为什么不能在头文件里定义全局变量?

头文件是C语言的一个重要组成部分,这种类型的文件名一般以 .h 结尾,h 表示 header,因此被称为“头文件”。头文件里一般存放公开的函数原型,数据类型等内容,其他模块需要使用这些函数或者数据类型时,只需包含相应头文件即可。

相信读者大都使用过C语言的头文件,不过还是有可能对其理解不透彻,这会导致读者在遇到一些问题时不知道如何解决。本文将较为详细的讨论C语言头文件的特点,并在此基础上,分析几个初学者常会跳进的“陷阱”,以及相应的解决办法。

C语言的#include语法

头文件通常与C语言的#include 语法配合使用,意为“将头文件内容包含进来”,例如在 t.c 文件里写下这段C语言代码:

#include <stdio.h>
int main()
{
    printf("hello world\n");
    return 0;
}

编译器在编译这段C语言代码之前,会有一个“预处理”的过程,在此过程中,stdio.h 里的内容被展开到 t.c 文件里。事实上,在终端输入 gcc -E 命令即可查看预处理后的C语言代码:

# gcc -E t.c


可见,编译器在预处理阶段会将 stdio.h 的内容展开到 main() 函数之前。事实上,如果创建 str.h 文件,并在其中写入“hello world\n”,我们甚至可以写出下面这样的C语言代码:

#include <stdio.h>
int main()
{
    printf(
#include "str.h"
    );
    return 0;
}

输入 gcc -E 查看编译器预处理后的C语言代码,会发现编译器将 str.h 文件里的内容“hello world\n”展开到 printf() 里了,此时 printf(#include "str.h"); 等价于 printf("hello world\n");,所以编译并执行这段C语言代码,得到如下输出:

# gcc t.c
# ./a.out 
hello world

到这里,相信读者已经发现,在C语言程序开发中,#include实际上就是把头文件里的内容复制到对应的位置。

避免C语言代码重复包含头文件

今天在我的交流群里有个小伙伴在编写C语言程序时,遇到了自己无法解决的错误。为了讨论主题,我把他的问题简化:创建 test.h 文件,并在其中定义一个全局变量:

// test.h 文件
int global_val = 0;

然后创建 t1.c 文件,使用 #include 包含该头文件,相应的C语言代码如下,请看:

// t1.c 文件
#include“test.h”
#include "stdio.h"
#include“test.h”
int main()
{
    printf("val = %d\n", ++global_val);
    return 0;
}

编译这段C语言代码,小伙伴发现编译器报错了:

错误信息提示变量 global_val 被重复定义,但是小伙伴查看自己的代码,发现只有 test.h 里一处定义了变量 global_val,这让他很迷惑。

小伙伴会感到迷惑,主要是因为对C语言的“头文件”机制理解不够深入,他认为只有 test.h 文件一处定义变量 global_val,不可能会导致“重复定义”错误的。

实际上,按照我们上面的分析, #include 包含头文件并没有什么特别的,它只是将头文件里的内容复制到 #include 处而已。知道了这一点,再看小伙伴的C语言代码,就一切明了了:他不小心(也有可能故意)包含了 test.h 文件两次,所以 test.h 文件里的内容会被赋值到 main() 函数之前两次,就相当于:

int global_val = 0;
int global_val = 0;
int main()...

这当然会引发“重复定义”的错误。解决错误的办法很简单,避免头文件被重复包含即可,所以删去一个#include "test.h" 就可以了。不过,我们能够轻易发现头文件被重复包含,是因为这里的代码很简单。如果C语言代码再复杂一点,或者多几层嵌套,就比较难发现头文件被重复包含了。

例如,test1.h 包含了 test2.h 文件,test2.h 文件包含了 test.h 文件。这种情况下,t.c 文件同时包含 test1.h 和 test.h 文件,一样会引起 test.h 文件被重复包含的。

在实际的C语言项目开发中,头文件一般都要加上预编译条件语句,比较常用的有 #ifdef#ifndef等。例如,将 test.h 文件做如下修改:

//test.h 文件
#ifndef __TEST_H__
#define __TEST_H__

int global_val = 0;

#endif

上述C语言代码中的 #ifndef#define 配合,可避免该头文件在同环境中被重复包含。所以即使 t.c 文件中写了多次 #include "test.h"文件,编译器也不会再报错:

// t1.c 文件
#include“test.h”
#include "stdio.h"
#include“test.h”
int main()
{
    printf("val = %d\n", ++global_val);
    return 0;
}

编译并执行这段C语言代码,可得如下输出:

# gcc t2.c
# ./a.out 
val = 1

初学者感到头疼的问题

有的读者知道使用 #ifdef 等条件编译语句避免头文件在同环境被重复包含,但还是有可能写出有问题的C语言程序。下面这个问题也是群里小伙伴提出的,为了讨论主题,我对其做了精简:小伙伴在 test.h 文件和 t1.c 文件的工程基础上,新建了 t2.c 文件,其中 t2.c 文件的内容如下:

// t2.c 文件
#include "test.h"
void add_val()
{
    global_val ++;
}

显然,t2.c 文件也包含了 test.h 头文件,并使用了其中定义的 global_val 变量。然后小伙伴在将 add_val() 函数的原型加入 test.h 头文件里:

// test.h 文件
#ifndef __TEST_H__
#define __TEST_H__

int global_val = 0;
void add_val();

#endif

接着,小伙伴在 t1.c 文件里调用了 add_val() 函数,相关C语言代码如下,请看:

// t1.c 文件
#include <stdio.h>
#include "test.h"

int main()
{
    add_val();
    printf("val = %d\n", ++global_val);

    return 0;
}

写好这些C语言代码后,发现编译报错了,依然是重复定义的错误,小伙伴感到非常迷惑。为什么写了预编译语句,还是出现这种错误呢?

答案其实很简单,预编译条件语句仅作用于同一环境。t1.c 和 t2.c 文件属于两个模块,因此#ifndef 不能避免 test.h 文件被 t1.c 和 t2.c 同时包含,这就会导致 int global_val = 0; 在整个C语言程序中有两处定义,编译器自然会报错。

小结

本文较为详细的介绍了C语言中头文件的性质,并在此基础上,分析了初学者常遇到的两个问题。应明白,在实际的C语言项目开发中,很少有程序员会在头文件里定义全局变量。作为延伸,如果本文中 test.h 文件里的 global_val 定义为 static 变量,那么编译就不会报错了。究竟为什么,以及加上 static 会带来什么样的变化,留给读者自己思考了。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif