头文件的作用分析

问题引入

假设有一个C/C++语言项目,项目中包含了很多模块,每个模块中又包含了很多功能函数。对于这个项目,稍稍学习过编程知识的开发者都会将模块做成动态或者静态库。在动态或者静态库中,往往包含了很多头文件和源文件。现在思考一个问题,为什么需要头文件?似乎从开始学习编程开始老师就教导我们要写头文件和源文件,但是有没有认真思考过它们的作用?本篇来通过一个简单例子来简要分析一下头文件的作用。

背景介绍

为了简化问题,我们假设需要开发一个可执行项目,在这个项目中需要一个模块,这个模块中仅仅包含两个函数,它们的作用就是打印出传入的参数值。代码如下:

 1 #include <stdio.h>
 2 
 3 void test(int val)
 4 {
 5     printf("test : %d\n", val);
 6 }
 7 
 8 void fun(int val)
 9 {
10     printf("fun : %d\n", val);
11 }

再增加main函数,代码如下:

1 int main(int argc, char* argv[])
2 {
3     test(123);
4     fun(456);
5     return 0;
6 }

不划分模块

最简单的情况就是将这个模块和main函数写在一个文件中,但是我们都知道这样做不但模块无法重用,同时也会给后面扩充功能带来繁重的工作。划分模块,可以将功能封装,隐藏实现细节,还可以更好的实现模块复用,并且独立模块间的低耦合也为了扩展升级提供了便利。

将这个源文件编译:

gcc -o nomodule main.c

使用头文件

加入头文件test.h,代码如下:

1 #ifndef TEST_H_
2 #define TEST_H_
3 
4 void test(int val);
5 void fun(int val);
6 
7 #endif //TEST_H_

修改main.c,代码如下:

1 #include "test.h"
2 
3 int main(int argc, char* argv[])
4 {
5     test(123);
6     fun(456);
7     return 0;
8 }

重新编译

gcc -o header main.c test.c

执行程序,可以看到效果。再来看下头文件的作用,使用gcc -E来查看预处理的结果

gcc -E test.c -o test.i

查看test.i,关键的代码如下:

1 # 1 "./test.h" 1
2 
3 void test(int val);
4 void fun(int val);

可以看到#include “test.h”被展开了,它的内容就是我们在头文件中写的两个函数声明。其实,这就是头文件的作用体现了,也就是说在test.c文件中将头文件中的内容加入了进来。可以再使用上述命令查看main.c文件,结果也是一样的。我们将i文件编译成o文件:

1 gcc -c test.i -o test.o
2 gcc -c main.i -o main.o

然后将o文件编译成可执行程序:

gcc -o header main.o test.o

执行与上一节结果是一样的。你会不会有疑问?难道可以不用头文件?别急,我们继续。

没有头文件

是的,这次没有头文件。

test.c中定义两个函数,内容省略。

在main.c中,代码如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 void test(int);
 5 void fun(int);
 6 
 7 int main(int argc, char* argv[])
 8 {
 9     test(123);
10     fun(456);
11     return 0;
12 }

将test.c编译为目标文件o文件

gcc -c test.c

再将main.c编译为目标文件

gcc -c main.c

将两个目标文件链接为可执行程序

gcc -o noheader main.o test.o

执行与前两节是一样的。至此,可以完全看明白头文件的作用了,它在预处理期间就被展开了,然后嵌入到源文件当中。

总结

通过以上三种情况的分析可以看到头文件在编译链接过程中的作用,从编译链接的角度来说,不写头文件也是行得通的,只要你不怕写N多函数或者类的声明就行。其实,函数声明只是在生成目标文件(*.o)时产生一个符号,具体符号的使用是在链接期间绑定的,而不同的编译方式(静态编译或动态编译)绑定的时机也不相同,以后再详细介绍。