头文件的作用分析
问题引入
假设有一个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)时产生一个符号,具体符号的使用是在链接期间绑定的,而不同的编译方式(静态编译或动态编译)绑定的时机也不相同,以后再详细介绍。