13

C语言陷阱与技巧第36节,#include包含C语言中的头文件是什么意思?为什么不能在头文件...

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E9%99%B7%E9%98%B1%E4%B8%8E%E6%8A%80%E5%B7%A7%E7%AC%AC36%E8%8A%82-include%E5%8C%85%E5%90%ABc%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%9A%84%E5%A4%B4%E6%96%87%E4%BB%B6%E6%98%AF/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

头文件是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
6b5c083c2a310eadd2392b3a2c2f8a91.png

可见,编译器在预处理阶段会将 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语言代码,小伙伴发现编译器报错了:

c38f9278080fcd3a99df11442095c993.png

错误信息提示变量 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语言代码后,发现编译报错了,依然是重复定义的错误,小伙伴感到非常迷惑。为什么写了预编译语句,还是出现这种错误呢?
52a19147433c9adcc9f2557ece50f50a.png

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK