头文件

对于 C 语言来说,头文件的设计体现了大部分的系统设计。不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上不合理的设计。

原则 1.1 头文件中适合放置接口的声明,不适合放置实现。

头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。

内部使用的函数(相当于类的私有方法)声明不应放在头文件中。

内部使用的宏、枚举、结构定义不应放入头文件中。

变量定义不应放在头文件中,应放在.c 文件中。

变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。 即使必须使用全局变量,也只应当在.c 中定义全局变量,在.h 中仅声明变量为全局的。

原则 1.2 头文件应当职责单一。

头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。很多现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能导致为了在.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 次,大大增加了工程的编译时间。

原则 1.3 头文件应向稳定的方向包含。

头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。

规则 1.1 每一个.c 文件应有一个同名.h 文件,用于声明需要对外公开的接口。

如果一个.c 文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如 main 函数所在的文件。 示例:对于如下场景,如在一个.c 中存在函数调用关系:

void foo()
{
  bar();
}
void bar()
{
  Do something;
}

必须在 foo 之前声明 bar,否则会导致编译错误。

这一类的函数声明,应当在.c 的头部声明,并声明为 static 的,如下:

static void bar();
void foo()
{
  bar();
}
void bar()
{
  Do something;
}

规则 1.2 禁止头文件循环依赖。

头文件循环依赖,指 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 的源代码重新编译。

规则 1.3 .c/.h 文件禁止包含用不到的头文件。

很多系统中头文件包含关系复杂,开发人员为了省事起见,可能不会去一一钻研,直接包含一切想到的头文件,甚至有些产品干脆发布了一个 god.h,其中包含了所有头文件,然后发布给各个项目组使用,这种只图一时省事的做法,导致整个系统的编译时间进一步恶化,并对后来人的维护造成了巨大的麻烦。

规则 1.4 头文件应当自包含。

简单的说,自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担。

规则 1.5 总是编写内部#include 保护符(#define 保护)。

多次包含一个头文件可以通过认真的设计来避免。如果不能做到这一点,就需要采取阻止头文件内容被包含多于一次的机制。

通常的手段是为每个文件配置一个宏,当头文件第一次被包含时就定义这个宏,并在头文件被再次包含时使用它以排除文件内容。

所有头文件都应当使用#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)前面。

规则 1.6 禁止在头文件中定义变量。

在头文件中定义变量,将会由于头文件被其他.c 文件包含而导致变量重复定义。

规则 1.7 只能通过包含头文件的方式使用其他.c 提供的接口,禁止在.c 中通过 extern 的方式使用外部函数接口、变量。

若 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 改变时可能导致声明和定义不一致。

规则 1.8 禁止在 extern "C"中包含头文件。

在 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”
{
...
}

一个模块通常包含多个.c 文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每一个模块提供一个.h,文件名为目录名。

需要注意的是,这个.h 并不是简单的包含所有内部的.h,它是为了模块使用者的方便,对外整体提供的模块接口。

建议 1.2 如果一个模块包含多个子模块,则建议每一个子模块提供一个对外的.h,文件名为子模块名。

降低接口使用者的编写难度。

建议 1.3 头文件不要使用非习惯用法的扩展名,如.inc。

目前很多产品中使用了.inc 作为头文件扩展名,这不符合 c 语言的习惯用法。在使用.inc 作为头文件扩展名的产品,习惯上用于标识此头文件为私有头文件。但是从产品的实际代码来看,这一条并没有被遵守,一个.inc 文件被多个.c 包含比比皆是。本规范不提倡将私有定义单独放在头文件中,具体见 规则 1.1。

建议 1.4 同一产品统一包含头文件排列方式。

常见的包含头文件排列方式:功能块排序、文件名升序、稳定度排序。

示例 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 的错误,可以部分减少编译时间。