在编译器只认识.c(.cpp))文件,而不知道.h是何物的年代,那时的人们写了很多的.c(.cpp)文件,渐渐地,人们发现在很多.c(.cpp)文件中的声明语句就是相同的,但他们却不得不一个字一个字地重复地将这些内容敲入每个.c(.cpp)文件。但更为恐怖的是,当其中一个声明有变更时,就需要检查所有的.c(.cpp)文件。
于是人们将重复的部分提取出来,放在一个新文件里,然后在需要的.c(.cpp)文件中敲入#include XXXX这样的语句。这样即使某个声明发生了变更,也再不需要到处寻找与修改了。因为这个新文件,经常被放在.c(.cpp)文件的头部,所以就给它起名叫做“头文件”,扩展名是.h。
在我们语言的初学阶段,往往我们的程序只有一个.c的文件或这很少的几个,这时我们就很少遇到头文件组织这个头疼的问题,随着我们程序的增加,代码量到了几千行甚至几万行,文件数也越来越多。这时这些文件的组织就成了一个问题,其实说白了这些文件的组织问题从理论上来说是软件工程中的模块设计的问题。
头文件的作用的功能和作用:
(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。
(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。
假定编译程序编译myproj.c(其中含main())时,发现它include了mylib.h(其中声明了函数void test()),那么此时编译器将按照事先设定的路径(Include路径列表及代码文件所在的路径)查找与之同名的实现文件(扩展名为.cpp或.c,此例中为mylib.c),如果找到该文件,并在其中找到该函数(此例中为void test())的实现代码,则继续编译;如果在指定目录找不到实现文件,或者在该文件及后续的各include文件中未找到实现代码,则返回一个编译错误。
其实include的过程完全可以“看成”是一个文件拼接的过程,将声明和实现分别写在头文件及C文件中,或者将二者同时写在头文件中,理论上没有本质的区别。
理论上来说.c文件与.h文件里的内容,只要是C语言所支持的,无论写什么都可以的,比如你在头文件中写函数体,只要在任何一个.c文件包含此头文件就可以将这个函数编译成目标文件的一部分(编译是以.c文件为单位的,如果不在任何.c文件中包含此头文件的话,这段代码就形同虚设),你可以在.c文件中进行函数声明,变量声明,结构体声明,这也不成问题。
那为何一定要分成头文件与.c文件呢,又为何一般都在.h文件中进行函数、变量声明、宏声明、结构体声明,而在.c文件中去进行变量定义,函数实现呢?
要理解.c文件与.h文件有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程:
1.预处理阶段
2.词法与语法分析阶段
3.编译阶段,首先编译成纯汇编语句,再将之汇编成跟CPU相关的二进制码,生成各个目标文件
4.连接阶段,将各个目标文件中的各段代码进行绝对地址定位,生成跟特定平台相关的可执行文件。
编译器在编译时是以.c文件为单位进行的,也就是说如果你的项目中一个.c文件都没有,那么你的项目将无法编译。
连接器是以目标文件为单位,它将一个或多个目标文件进行函数与变量的重定位,生成最终的可执行文件。
在PC上的程序开发,一般都有一个main函数,这是各个编译器的约定。为了生成一个最终的可执行文件,就需要一些目标文件,也就是需要.c文件,而这些.c文件中又需要一个main函数作为可执行程序的入口。
简单些说就是C语言的编译分为预处理、编译、汇编、链接(test.c test.h => test.i => test.s => test.o => test)四个大的阶段。
.c文件中的#include宏处理,会在预处理的阶段将.c中引用的.h文件的内容全部写到.c文件中,最后生成.i中间文件,这时.h 文件中的内容就相当于被写到c文件中。这也为代码的复用提供了渠道,很多的.c文件可以去引用同一个.h文件,这样这个.h文件就会被放到多个.c文件中被编译多次,这也是h文件中不能放定义只能放声明的原因:放定义时被编译多次,在程序链接的时候(系统中定义了多个int a;强符号定义)会出现错误。
.h文件里的声明就不一样,声明表示对定义的扩展,最终都会终结到一个定义上,所以不会出现link时重复定义的错误。
编程中我们在h文件中肯定都用过以下的格式
它的作用就是在h文件互相引用时,消除重复定义。当然宏定义是在预处理阶段发挥作用的,编译后的过程是没有宏的影子的。
上面的D文件中就会重复出现两个int a();的声明,这样就有点重复了,这时条件编译宏就派上了用场。不会重复定义了