对于 C 语言来说,头文件的设计体现了大部分的系统设计。不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上不合理的设计。
头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。
内部使用的函数(相当于类的私有方法)声明不应放在头文件中。
内部使用的宏、枚举、结构定义不应放入头文件中。
变量定义不应放在头文件中,应放在.c 文件中。
变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。 即使必须使用全局变量,也只应当在.c 中定义全局变量,在.h 中仅声明变量为全局的。
头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。很多现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能导致为了在.c 中使用一个宏,而包含十几个头文件。
示例:如下是某平台定义 WORD 类型的头文件:
#include <VXWORKS.H>
#include <KERNELLIB.H>
#include <SEMLIB.H>
#include <INTLIB.H>
#include <TASKLIB.H>
#include <MSGQLIB.H>
#include <STDARG.H>
#include <FIOLIB.H>
#include <STDIO.H>
#include <STDLIB.H>
#include <CTYPE.H>
#include <STRING.H>
#include <ERRNOLIB.H>
#include <TIMERS.H>
#include <MEMLIB.H>
#include <TIME.H>
#include <WDLIB.H>
#include <SYSLIB.H>
#include <TASKHOOKLIB.H>
#include <REBOOTLIB.H>
...
typedef unsigned short WORD;
...
这个头文件不但定义了基本数据类型 WORD,还包含了 stdio.h syslib.h 等等不常用的头文件。如果工程中有 10000 个源文件,而其中 100 个源文件使用了 stdio.h 的 printf,由于上述头文件的职责过于庞大,而 WORD 又是每一个文件必须包含的,从而导致 stdio.h/syslib.h 等可能被不必要的展开了 9900 次,大大增加了工程的编译时间。
头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。
如果一个.c 文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如 main 函数所在的文件。 示例:对于如下场景,如在一个.c 中存在函数调用关系:
void foo()
{
bar();
}
void bar()
{
Do something;
}
必须在 foo 之前声明 bar,否则会导致编译错误。
这一类的函数声明,应当在.c 的头部声明,并声明为 static 的,如下:
static void bar();
void foo()
{
bar();
}
void bar()
{
Do something;
}
头文件循环依赖,指 a.h 包含 b.h,b.h 包含 c.h,c.h 包含 a.h 之类导致任何一个头文件修改,都导致所有包含了 a.h/b.h/c.h 的代码全部重新编译一遍。而如果是单向依赖,如 a.h 包含 b.h,b.h 包含 c.h,而 c.h 不包含任何头文件,则修改 a.h 不会导致包含了 b.h/c.h 的源代码重新编译。
很多系统中头文件包含关系复杂,开发人员为了省事起见,可能不会去一一钻研,直接包含一切想到的头文件,甚至有些产品干脆发布了一个 god.h,其中包含了所有头文件,然后发布给各个项目组使用,这种只图一时省事的做法,导致整个系统的编译时间进一步恶化,并对后来人的维护造成了巨大的麻烦。
简单的说,自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担。
多次包含一个头文件可以通过认真的设计来避免。如果不能做到这一点,就需要采取阻止头文件内容被包含多于一次的机制。
通常的手段是为每个文件配置一个宏,当头文件第一次被包含时就定义这个宏,并在头文件被再次包含时使用它以排除文件内容。
所有头文件都应当使用#define 防止头文件被多重包含,命名格式为 FILENAME_H,为了保证唯一性,更好的命名是 PROJECTNAME_PATH_FILENAME_H。
注:没有在宏最前面加上“_",即使用 FILENAME_H 代替_FILENAMEH,是因为一般以""和”__"开头的标识符为系统保留或者标准库使用,在有些静态检查工具中,若全局可见的标识符以""开头会给出告警。
定义包含保护符时,应该遵守如下规则:
1)保护符使用唯一名称; 2)不要在受保护部分的前后放置代码或者注释。 示例:假定 VOS 工程的 timer 模块的 timer.h,其目录为 VOS/include/timer/timer.h,应按如下方式保护:
#ifndef VOS_INCLUDE_TIMER_TIMER_H
#define VOS_INCLUDE_TIMER_TIMER_H
...
#endif
也可以使用如下简单方式保护:
#ifndef TIMER_H
#define TIMER_H
..
#endif
例外情况:头文件的版权声明部分以及头文件的整体注释部分(如阐述此头文件的开发背景、使用注意事项等)可以放在保护符(#ifndef XX_H)前面。
在头文件中定义变量,将会由于头文件被其他.c 文件包含而导致变量重复定义。
若 a.c 使用了 b.c 定义的 foo()函数,则应当在 b.h 中声明 extern int foo(int input);并在 a.c 中通过#include <b.h>来使用 foo。禁止通过在 a.c 中直接写 extern int foo(int input);来使用 foo,后面这种写法容易在 foo 改变时可能导致声明和定义不一致。
在 extern "C"中包含头文件,会导致 extern "C"嵌套,Visual Studio 对 extern "C"嵌套层次有限制,嵌套层次太多会编译错误。
在 extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏。例如,存在 a.h 和 b.h 两个头文件:
#ifndef A_H**
#define A_H**
#ifdef \_\_cplusplus
void foo(int);
#define a(value) foo(value)
#else
void a(int)
#endif
#endif /_ A_H\_\_ _/
#ifndef B_H**
#define B_H**
#ifdef \_\_cplusplus
extern "C" {
#endif
#include "a.h"
void b();
#ifdef \_\_cplusplus
}
#endif
#endif /_ B_H\_\_ _/
使用 C++预处理器展开 b.h,将会得到
extern "C" {
void foo(int);
void b();
}
按照 a.h 作者的本意,函数 foo 是一个 C++自由函数,其链接规范为"C++"。但在 b.h 中,由于#include "a.h"被放到了 extern "C" { }的内部,函数 foo 的链接规范被不正确地更改了。
示例:错误的使用方式:
extern “C”
{
#include “xxx.h”
...
}
正确的使用方式:
#include “xxx.h”
extern “C”
{
...
}
需要注意的是,这个.h 并不是简单的包含所有内部的.h,它是为了模块使用者的方便,对外整体提供的模块接口。
降低接口使用者的编写难度。
目前很多产品中使用了.inc 作为头文件扩展名,这不符合 c 语言的习惯用法。在使用.inc 作为头文件扩展名的产品,习惯上用于标识此头文件为私有头文件。但是从产品的实际代码来看,这一条并没有被遵守,一个.inc 文件被多个.c 包含比比皆是。本规范不提倡将私有定义单独放在头文件中,具体见 规则 1.1。
常见的包含头文件排列方式:功能块排序、文件名升序、稳定度排序。
示例 1:
以升序方式排列头文件可以避免头文件被重复包含,如:
#include <a.h>
#include <b.h>
#include <c/d.h>
#include <c/e.h>
#include <f.h>
示例 2:
以稳定度排序,建议将不稳定的头文件放在前面,如把产品的头文件放在平台的头文件前面,如下:
#include <product.h>
#include <platform.h>
相对来说,product.h 修改的较为频繁,如果有错误,不必编译 platform.h 就可以发现 product.h 的错误,可以部分减少编译时间。