背景
在编译的时候,出现“redefine”的错误,最后检查才发现对应的头文件没有写正确的预编译信息:
#ifndef _HeadFileName_H
#define _HeadFileName_H
// 头文件内容
#endif //_HeadFileName_H
添加后,不再报错,然后就思考,这个“#ifndef #define #endif”的作用到底是什么?于是有了此篇文章。
正文
“#ifndef #define #endif”其实是预编译的一种机制。
在我们的C project中,通常由“.c”“.h”文件构成,“.c”文件即是具体的代码,相应的“.h”文件即是对应“.c”文件的说明。
在预编译的时候,编译器会将“.c”包含的所有的“.h”文件内的函数,宏定义,变量信息给汇聚到一个文件内,但能被写入该文件的条件是,所有的这些函数入口,宏定义,变量信息有且只能有一个,因此,当有多个“.c”文件包含同一个“.h”文件时,就会出现重复定义的错误,所以“#ifndef”、“#define”、“#endif”就派上用场了,当编译器第一次预编译一个“.h”文件时,该文件内的宏则还没有被定义,然后编译器则会先定义该宏,并将该“.h”文件内的信息记录,待第二次编译器又预编译到同一个“.h”文件时,该宏定义已经被定义,则编译器直接跳转到“#endif”,略过此包含。
形象点比喻,整个project就是一个由可以实现各种功能的的厂房组成的工厂,每个“.c”文件对应的即是工厂内的每一个厂房,对应的“.h”则挂在对应的厂房外,标明了这个“.c”厂房内有哪些设备,可以实现哪些功能。而另外一些“.c”厂房如果需要用其他厂房的功能,只需要在他的“.h”标示文件内将其他厂房内的“.h”标示文件写在其标示文件内即可,这就是文件包含(相当于在“.h”文件内包含其他“.h”文件);或者直接在他的“.h”文件旁,挂其他厂房的“.h”标示文件(相当于在.c文件包含其他“.h”文件)。
在实际运行时,整个工厂就相当于一个流水线开始运作了,当运作到“A.c”A厂房的时候,在这个环节内,需要使用“B.c”B厂房内的一个功能,这个流水线就会根据A厂房外挂的B厂房的标示文件“B.h”找到B厂房对应功能的工位地址,接着去运行对应的功能。这就是“.c”、“.h”的功能。
预编译阶段,就相当于将这个工厂组合起来的组织者(编译器)实现工厂整合的过程,可是这个组织者比较笨。他会按照顺序巡视所有的厂房,譬如A厂房,然后一看A厂房的“.h”标示板,就会用个文件记下来,A厂房有功能1,功能2。在巡视A厂房的时候,它看到了B厂房的标识板,组织者(编译器)如实记录A厂房还有B厂房的功能3,功能4,然后检查了下A厂房的设备都没问题(C文件内没有语法错误)。接着组织者(编译器)巡视到在B厂房的标识板时,看到B厂房也有功能3,功能4.组织者(编译器)就不干了,就说这个功能A厂房已经有了,不能重复建设,不环保(- -!),项目不能通过验收,回去重做(报redefine的错误)。
于是工厂建造者(我们程序员)就想起来一个方法“#ifndef #define #endif”,和编译器约定了一个规则,如果碰到标示板(“.h”)文件上没有定义对应的头文件的宏,那就定义它,然后将里面的功能给记录下来,以#endif表示结尾。如下所示,
#ifndef _HeadFileName_H
#define _HeadFileName_H
// 头文件内容
#endif //_HeadFileName_H
接着,编译器看到同一个头文件对应的宏定义之前已经定义了,说明这些功能已经记录,编译器就会直接跳转到对应的#endif位置,无视此次包含。由于这些功能此前已经记录,因此不会出现在实际运行时找不到对应的功能入口地址的情况。
以上说的既是多个文件包含同一个头文件,编译器的具体行为。
要强调一点,以上所说的头文件内一定不要定义变量,只能定义宏定义、enmu,函数声明。因为,定义后者,编译器只需要将对应的记号记录到一个文件内即可,定义前者,编译器一定会给它分配实体地址,第二次再来包含的时候,若是在头文件内有变量,有些编译器会无视“#ifndef #define #endif”的约定并再次为它分配地址,然后编译器发现之前已经分配过地址了,还是会报“redefine”的错误。当然只是有些笨的编译器会,已经证实gcc编译器不会有这种笨的处理,但是,为了使自己的代码能兼容更多的的编译器,还是建议不要在“.h”文件内定义变量。
至此,记录完毕。
参考链接
Why use #ifndef CLASS_H and #define CLASS_H in .h file but not in .cpp?;
记录时间:2017-1-6
记录地点:深圳WZ