C语言的头文件包含竟然有那么多讲究

前言很多事不深入以为自己懂了,但真正用到项目上,才发现了问题 。曾以为自己写C语言已经轻车熟路了,特别是对软件文件的工程管理上,因为心里对自己的代码编写风格还是有自信的 。(毕竟刚毕业时老大对我最初的训练就是编码格式的规范化处理)
曾以为,一个.c文件对应一个.h文件,.c文件只包含它自身的.h文件就好,若.c文件中用到其他文件中的内容,则.h文件把用到的头文件包含进来就可以了 。自己貌似一直秉承这个理念在进行代码编写(好可怕) 。工程文件数量小时,这种理念貌似看不出问题,但随着工程文件数量越来越多,我发现自己这种思路有了弊端:头文件互相包含,导致编译时自以为有些宏变量声明了,它就能起作用,但实际测试发现这种方式编码后,有些声明的宏没能起到作用 。
经过领导及同事的指正,自己才明白原有的代码编写习惯不正确 。应该秉承.c文件对应的.h文件只包含头文件里用到的其它文件的头文件,任何非必须的.h文件不要包含;而.c文件里面要包含用到的所有.h文件 。这样写即使存在.c文件内头文件重复包含也不伤大雅 。
语言描述有时太抽象,还是符号举例说明下:假如有两个.c文件分别为A.c和B.c,自然它们都有各自的A.h和B.h文件 。
原有的思路:A.c里面只有一个#include "A.h",而A.h所包含的就是一大堆如B.h,C.h,D.h.....文件,因为A.c文件里面要用到B.h,C.h,D.h里面的内容 。如图一所示 。
新思路:A.h里面只包含A.h所写内容要用到的.h文件,很多时候A.h里面无需任何.h文件.而在A.c文件内就要写成 #include "B.h" #include "C.h" #include "D.h" 。而且两个文件的.c文件在头文件包含上可以互相包含 。如图二所示 。

C语言的头文件包含竟然有那么多讲究

文章插图
 

C语言的头文件包含竟然有那么多讲究

文章插图
 
项目中遇到的这个头文件包含问题导致我重新搜索资料进行该问题的深入了解,故下文是通过网络资源的搜查及加上自己对它的理解,进行了相关内容的整理,希望对感兴趣的小伙伴有所帮助 。
背景对于C语言来说,头文件的设计体现了大部分的系统设计 。不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上不合理的设计 。
依赖特指编译依赖 。若x.h包含了y.h,则称作x依赖y 。依赖关系会进行传导,如x.h包含y.h,而y.h又包含了z.h,则x通过y依赖了z 。依赖将导致编译时间的上升 。虽然依赖是不可避免的,也是必须的,但是不良的设计会导致整个系统的依赖关系无比复杂,使得任意一个文件的修改都要重新编译整个系统,导致编译时间巨幅上升 。
在一个设计良好的系统中, 修改一个文件,只需要重新编译数个,甚至是一个文件 。
某产品曾经做过一个实验,把所有函数的实现通过工具注释掉,其编译时间只减少了不到10%,究其原因,在于A包含B, B包含C, C包含D,最终几乎每一个源文件都包含了项目组所有的头文件,从而导致绝大部分编译时间都花在解析头文件上 。
某产品更有一个“优秀实践”,用于将.c文件通过工具合并成一个比较大的.c文件,从而大幅度提高编译效率 。其根本原因还是在于通过合并.c文件减少了头文件解析次数 。但是,这样的“优秀实践”是对合理划分.c文件的一种破坏 。
大部分产品修改一处代码,都得需要编译整个工程,对于TDD之类的实践,要求对于模块级别的编译时间控制在秒级,即使使用分布式编译也难以实现,最终仍然需要合理的划分头文件、以及头文件之间的包含关系, 从根本上降低编译时间 。
《google C++ Style Guide》 1.2 头文件依赖 章节也给出了类似的阐述:


    推荐阅读