2 头文件

通常每一个 C++源文件(.cc,.C,.cpp,.cxx) 文件都有一个对应的 .h 文件。也有一些例外,如单元测试代码和仅包含 main() 函数的较小的 .cc 文件。

正确使用头文件可令代码在可读性、文件大小和性能上大为改观。

下面的规则可规避使用头文件时的各种陷阱。

2.1 Self-contained 头文件

规则:头文件应该是独立的(Self-contained,解释:单独编译(Compile on their own)),并以 .h 为后缀。可被其它文件包含(#include)的非头文件应以 .inc 为后缀。

所有头文件都是独立的。换言之,用户和重构工具不需要为特定场合而包含额外的头文件。详言之,一个头须包含它所需要的所有其它头文件,并符合第 2.2 节之规定。

规则:模板和内联函数的声明及定义应放在同一个头文件中,凡是用到模板或内联函数的源文件必须包含定义它们的头文件,否则 Build 时可能会产生链接错误。不要把这些定义放到分离的-inl.h 文件里。

例外情况:如果某函数模板为所有相关模板参数显式实例化,或本身就是某类的一个私有成员,那么它就只能定义在实例化该模板的源文件里。

有极少数情况下,一些可包含文件并不是 self-contained 的,此类往往包含在一些不寻常的位置,例如在另一个文件中间。它们没有第 5.2 节规定的防多重包含保护,也没有包含一些先决条件。这些文件要用.inc 为扩展名。请谨慎使用这种做法,尽可能使用 Self-contained 的头文件。

2.2 #define 保护

规则:所有头文件都应使用#define 来防止被多重包含,推荐格式:

<PROJECT>_<PATH>_<FILE>_H_

为保证唯一性,头文件的命名应基于所在项目源代码树的全路径。例如,项目 foo 中的头文件 foo/src/bar/baz.h 可按如下方式保护:

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_

2.3 前置声明

规则:尽量避免使用前置声明,需要时,#include 所需的头文件即可。

定义:「前置声明」(Forward declaration)是指类、结构体、函数或模板的纯粹声明,没伴随着其定义。

优点:

  • 能够节省编译时间,因为#includes 会迫使编译器展开更多的文件,处理更多的输入;
  • 能够节省不必要的重新编译的时间,#includes 会使代码因为头文件中无关的改动而被重新编译多次。

缺点:

  • 会隐藏依赖关系,头文件改动时,代码会跳过必要的重新编译过程;
  • 可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其API。例如:扩大形参类型、增加自带默认参数的模板形参或者迁移到新的命名空间;
  • 前置声明来自命名空间 std:: 的 symbols 时,其行为未定义;
  • 很难判断什么时候该用前置声明,什么时候该用 #include。用前置声明代替#include 可能会改变代码的含义:
// b.h:
struct B {};
struct D : B {};
// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); } // calls f(B*)

如果#include 被 B 和 D 的前置声明替代, test()就会调用 f(void*);

  • 前置声明了多个来自头文件的 symbols 时,就会比仅有一行的 #include 冗长;
  • 仅仅为了能前置声明而构造代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂。

结论:

  • 尽量避免对定义在其他项目中的实体进行前置声明;
  • 使用在头文件中声明的函数时,应采用#include 方式;
  • 使用类模板时,优先使用 #include 来包含头文件。

关于什么时候包含头文件,参见第 2.5 节。

2.4 内联函数

规则 : 函数体较小时,比如代码少于 10 行,可将其定义为内联函数。

定义 : 当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用。

优点 : 内联函数的函数体较小时, 目标代码的执行效率会更高。存取器(Accessors)、修改器(Mutators)、函数体小的函数和性能关键的函数,尽量使用内联方式。

缺点 : 滥用内联会导致程序变得更慢。内联可能使目标代码量或增或减,这取决于内联函数的大小。内联非常短小的存取器函数通常会减少代码大小,但内联一个相当大的函数将戏剧性的增加代码大小。现代处理器由于更好的利用了指令缓存,小巧的代码往往执行更快。

结论 : 不要内联超过 10 行的函数。析构函数通常都会有隐含的代码,所以函数体会比表面看起来要长,内联析构函数时一定要注意这一点。

尽量不要内联那些包含循环或 switch 语句的函数,除非这些循环或 switch 语句在大多数情况下都不会被执行。

有些函数即使声明为内联的也不一定会被编译器内联,这点很重要。比如虚函数和递归函数就不会被正常内联。通常,递归函数不应该声明成内联函数。虚函数内联的主要原因是想把它的函数体放在类定义内,或许是为了图个方便,抑或是当作文档描述其行为,比如精短的存取函数。

2.5 Includes 的路径及顺序

规则 : 标准的头文件包含顺序为:相关头文件,C 库,C++库,其他库,本项目内的.h。使用标准头文件包含顺序能增强可读性,避免隐藏依赖。

规则:项目内头文件应按照项目源代码目录树结构排列,尽量避免使用特殊的快捷目录. (当前目录) 或.. (上级目录)。

例如,xxx-project/src/base/logging.h 应按如下方式包含头文件:

#include "base/logging.h"

又如,dir/foo.cc 或者 dir/foo_test.cc 的主要作用是实现或测试 dir2/foo2.h 的功能,包含头文件的次序如下:

  1. dir2/foo2.h (优先位置, 详情如下)
  2. C 系统文件
  3. C++系统文件
  4. 其他库的.h 文件
  5. 本项目内.h 文件

采用这种优先的顺序,在 dir2/foo2.h 遗漏某些必要的库时, dir/foo.cc 或 dir/foo_test.cc 的编译会立刻中止。因此这一条规则能确保使用这些文件的人员首先看到编译中止的消息。

规则 : 按字母顺序对头文件进行排序。

规则 : 应包含定义了当前文件依赖的所有 symbols 定义的头文件,前置声明的情况除外。

例如,某个文件中用到了 bar.h 中的某个 symbol,即使该文件已包含的头文件 foo.h 中已经包含了 bar.h,也需要在该文件中包含 bar.h,除非 foo.h 有明确说明它会自动提供 bar.h 中的 symbol。

又如,xxx-project/src/foo/internal/fooserver.cc 的包含次序如下:

#include "foo/server/fooserver.h"
#include <sys/types.h>
#include <unistd.h>
#include <hash_map>
#include <vector>
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/server/bar.h"

规则 : 系统/平台特定代码要用条件包含(conditional includes),此类代码应放到其它includes 语句之后。同时,此类代码应简洁、独立。

例如:

#include "foo/public/fooserver.h"
#include "base/port.h" // For LANG_CXX11
#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11

results matching ""

    No results matching ""