头文件
头文件职责
头文件是模块或文件的对外接口,头文件的设计体现了大部分的系统设计。头文件中适合放置接口的声明,不适合放置实现(内联函数除外)。对于cpp文件中内部才需要使用的函数、宏、枚举、结构定义等不要放在头文件中。头文件应当职责单一。头文件过于复杂,依赖过于复杂还是导致编译时间过长的主要原因。
建议5.1.1 每一个.cpp文件应有一个对应的.h文件,用于声明需要对外公开的类与接口
通常情况下,每个.cpp文件都有一个相应的.h,用于放置对外提供的函数声明、宏定义、类型定义等。如果一个.cpp文件不需要对外公布任何接口,则其就不应当存在。例外:程序的入口(如main函数所在的文件),单元测试代码,动态库代码。
示例:
// Foo.h
#ifndef FOO_H
#define FOO_H
class Foo {
public:
Foo();
void Fun();
private:
int value;
};
#endif
// Foo.cpp
#include "Foo.h"
namespace { // Good: 对内函数的声明放在.cpp文件的头部,并声明为匿名namespace或者static限制其作用域
void Bar()
{
}
}
...
void Foo::Fun() {
Bar();
}
头文件依赖
规则5.2.1 禁止头文件循环依赖
头文件循环依赖,指 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的源代码重新编译。
头文件循环依赖直接体现了架构设计上的不合理,可通过优化架构去避免。
规则5.2.2 禁止包含用不到的头文件
用不到的头文件被包含的同时引入了不必要的依赖,增加了模块或单元之间的耦合度,只要该头文件被修改,代码就要重新编译。
很多系统中头文件包含关系复杂,开发人员为了省事起见,直接包含一切想到的头文件,甚至发布了一个god.h,其中包含了所有头文件,然后发布给各个项目组使用,这种只图一时省事的做法,导致整个系统的编译时间进一步恶化,并对后来人的维护造成了巨大的麻烦。
规则5.2.3 头文件应当自包含
简单的说,自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,给这个头文件的用户增添不必要的负担。
示例:如果a.h不是自包含的,需要包含b.h才能编译,会带来的危害:每个使用a.h头文件的.cpp文件,为了让引入的a.h的内容编译通过,都要包含额外的头文件b.h。额外的头文件b.h必须在a.h之前进行包含,这在包含顺序上产生了依赖。
规则5.2.4 头文件必须编写#define保护,防止重复包含
为防止头文件被重复包含,所有头文件都应当使用 #define 保护;不要使用 #pragma once
定义包含保护符时,应该遵守如下规则: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
建议5.2.1 禁止通过声明的方式引用外部函数接口、变量
只能通过包含头文件的方式使用其他模块或文件提供的接口。通过 extern 声明的方式使用外部函数接口、变量,容易在外部接口改变时可能导致声明和定义不一致。同时这种隐式依赖,容易导致架构腐化。
不符合规范的案例:
// a.cpp内容
extern int Fun(); // Bad: 通过extern的方式使用外部函数
void Bar() {
int i = Fun();
...
}
// b.cpp内容
int Fun() {
// Do something
}
应该改为:
// a.cpp内容
#include "b.h" // Good: 通过包含头文件的方式使用其他.cpp提供的接口
void Bar() {
int i = Fun();
...
}
// b.h内容
int Fun();
// b.cpp内容
int Fun() {
// Do something
}
例外,有些场景需要引用其内部函数,但并不想侵入代码时,可以 extern 声明方式引用。如:针对某一内部函数进行单元测试时,可以通过 extern 声明来引用被测函数;当需要对某一函数进行打桩、打补丁处理时,允许 extern 声明该函数。
规则5.2.5 禁止在extern "C"中包含头文件
在 extern "C" 中包含头文件,有可能会导致 extern "C" 嵌套,部分编译器对 extern "C" 嵌套层次有限制,嵌套层次太多会编译错误。
在C,C++混合编程的情况下,在extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏,比如链接规范被不正确地更改。
示例,存在a.h和b.h两个头文件:
// a.h内容
...
#ifdef __cplusplus
void Foo(int);
#define A(value) Foo(value)
#else
void A(int)
#endif
// b.h内容
...
#ifdef __cplusplus
extern "C" {
#endif
#include "a.h"
void B();
#ifdef __cplusplus
}
#endif
使用C++预处理器展开b.h,将会得到
extern "C" {
void Foo(int);
void B();
}
按照 a.h 作者的本意,函数 Foo 是一个 C++ 自由函数,其链接规范为 "C++"。但在 b.h 中,由于 #include "a.h"
被放到了 extern "C"
的内部,函数 Foo 的链接规范被不正确地更改了。
例外:如果在 C++ 编译环境中,想引用纯C的头文件,这些C头文件并没有extern "C"
修饰。非侵入式的做法是,在 extern "C"
中去包含C头文件。
建议5.2.2尽量避免使用前置声明,而是通过#include来包含头文件
前置声明(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义。
- 优点:
- 前置声明能够节省编译时间,多余的 #include 会迫使编译器展开更多的文件,处理更多的输入。
- 前置声明能够节省不必要的重新编译的时间。 #include 使代码因为头文件中无关的改动而被重新编译多次。
- 缺点:
- 前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
- 前置声明可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。
- 前置声明来自命名空间
std::
的 symbol 时,其行为未定义(在C++11标准规范中明确说明)。 - 前置声明了不少来自头文件的 symbol 时,就会比单单一行的 include 冗长。
- 仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂。
- 很难判断什么时候该用前置声明,什么时候该用
#include
,某些场景下面前置声明和#include
互换以后会导致意想不到的结果。所以我们尽可能避免使用前置声明,而是使用#include头文件来保证依赖关系。
建议5.2.3 头文件包含顺序:首先是.cpp相应的.h文件,其它头文件按照稳定度排序
使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖,建议按照稳定度排序:cpp对应的头文件, C/C++标准库, 系统库的.h, 其他库的.h, 本项目内其他的.h。
举例,Foo.cpp中包含头文件的次序如下:
#include "Foo/Foo.h"
#include <cstdlib>
#include <string>
#include <linux/list.h>
#include <linux/time.h>
#include "platform/Base.h"
#include "platform/Framework.h"
#include "project/public/Log.h"
将Foo.h放在最前面可以保证当Foo.h遗漏某些必要的库,或者有错误时,Foo.cpp的构建会立刻中止,减少编译时间。 对于头文件中包含顺序也参照此建议。
例外:平台特定代码需要条件编译,这些代码可以放到其它 includes 之后。
#include "foo/public/FooServer.h"
#include "base/Port.h" // For LANG_CXX11.
#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11