• 头文件
    • 头文件职责
      • 建议5.1.1 每一个.cpp文件应有一个对应的.h文件,用于声明需要对外公开的类与接口
    • 头文件依赖
      • 规则5.2.1 禁止头文件循环依赖
      • 规则5.2.2 禁止包含用不到的头文件
      • 规则5.2.3 头文件应当自包含
      • 规则5.2.4 头文件必须编写#define保护,防止重复包含
      • 建议5.2.1 禁止通过声明的方式引用外部函数接口、变量
      • 规则5.2.5 禁止在extern "C"中包含头文件
      • 建议5.2.2尽量避免使用前置声明,而是通过#include来包含头文件
      • 建议5.2.3 头文件包含顺序:首先是.cpp相应的.h文件,其它头文件按照稳定度排序

    头文件

    头文件职责

    头文件是模块或文件的对外接口,头文件的设计体现了大部分的系统设计。头文件中适合放置接口的声明,不适合放置实现(内联函数除外)。对于cpp文件中内部才需要使用的函数、宏、枚举、结构定义等不要放在头文件中。头文件应当职责单一。头文件过于复杂,依赖过于复杂还是导致编译时间过长的主要原因。

    建议5.1.1 每一个.cpp文件应有一个对应的.h文件,用于声明需要对外公开的类与接口

    通常情况下,每个.cpp文件都有一个相应的.h,用于放置对外提供的函数声明、宏定义、类型定义等。如果一个.cpp文件不需要对外公布任何接口,则其就不应当存在。例外:程序的入口(如main函数所在的文件),单元测试代码,动态库代码。

    示例:

    1. // Foo.h
    2. #ifndef FOO_H
    3. #define FOO_H
    4. class Foo {
    5. public:
    6. Foo();
    7. void Fun();
    8. private:
    9. int value;
    10. };
    11. #endif
    1. // Foo.cpp
    2. #include "Foo.h"
    3. namespace { // Good: 对内函数的声明放在.cpp文件的头部,并声明为匿名namespace或者static限制其作用域
    4. void Bar()
    5. {
    6. }
    7. }
    8. ...
    9. void Foo::Fun() {
    10. Bar();
    11. }

    头文件依赖

    规则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,应按如下方式保护:

    1. #ifndef VOS_INCLUDE_TIMER_TIMER_H
    2. #define VOS_INCLUDE_TIMER_TIMER_H
    3. ...
    4. #endif

    也可以不用像上面添加路径,但是要保证当前工程内宏是唯一的。

    1. #ifndef TIMER_H
    2. #define TIMER_H
    3. ...
    4. #endif

    建议5.2.1 禁止通过声明的方式引用外部函数接口、变量

    只能通过包含头文件的方式使用其他模块或文件提供的接口。通过 extern 声明的方式使用外部函数接口、变量,容易在外部接口改变时可能导致声明和定义不一致。同时这种隐式依赖,容易导致架构腐化。

    不符合规范的案例:

    // a.cpp内容

    1. extern int Fun(); // Bad: 通过extern的方式使用外部函数
    2. void Bar() {
    3. int i = Fun();
    4. ...
    5. }

    // b.cpp内容

    1. int Fun() {
    2. // Do something
    3. }

    应该改为:

    // a.cpp内容

    1. #include "b.h" // Good: 通过包含头文件的方式使用其他.cpp提供的接口
    2. void Bar() {
    3. int i = Fun();
    4. ...
    5. }

    // b.h内容

    1. int Fun();

    // b.cpp内容

    1. int Fun() {
    2. // Do something
    3. }

    例外,有些场景需要引用其内部函数,但并不想侵入代码时,可以 extern 声明方式引用。如:针对某一内部函数进行单元测试时,可以通过 extern 声明来引用被测函数;当需要对某一函数进行打桩、打补丁处理时,允许 extern 声明该函数。

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

    在 extern "C" 中包含头文件,有可能会导致 extern "C" 嵌套,部分编译器对 extern "C" 嵌套层次有限制,嵌套层次太多会编译错误。

    在C,C++混合编程的情况下,在extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏,比如链接规范被不正确地更改。

    示例,存在a.h和b.h两个头文件:

    // a.h内容

    1. ...
    2. #ifdef __cplusplus
    3. void Foo(int);
    4. #define A(value) Foo(value)
    5. #else
    6. void A(int)
    7. #endif

    // b.h内容

    1. ...
    2. #ifdef __cplusplus
    3. extern "C" {
    4. #endif
    5. #include "a.h"
    6. void B();
    7. #ifdef __cplusplus
    8. }
    9. #endif

    使用C++预处理器展开b.h,将会得到

    1. extern "C" {
    2. void Foo(int);
    3. void B();
    4. }

    按照 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中包含头文件的次序如下:

    1. #include "Foo/Foo.h"
    2. #include <cstdlib>
    3. #include <string>
    4. #include <linux/list.h>
    5. #include <linux/time.h>
    6. #include "platform/Base.h"
    7. #include "platform/Framework.h"
    8. #include "project/public/Log.h"

    将Foo.h放在最前面可以保证当Foo.h遗漏某些必要的库,或者有错误时,Foo.cpp的构建会立刻中止,减少编译时间。 对于头文件中包含顺序也参照此建议。

    例外:平台特定代码需要条件编译,这些代码可以放到其它 includes 之后。

    1. #include "foo/public/FooServer.h"
    2. #include "base/Port.h" // For LANG_CXX11.
    3. #ifdef LANG_CXX11
    4. #include <initializer_list>
    5. #endif // LANG_CXX11