头文件(Header Files)
一般来说,每个 .cc 文件都应该有一个对应的 .h
文件。有一些常见的例外,例如单元测试和只包含 main() 函数的小型 .cc
文件。
正确使用头文件可以对代码的可读性、大小和性能产生巨大影响。
以下规则将指导你避免使用头文件时的各种陷阱。
自包含头文件(Self-contained Headers)
头文件应该是自包含的(能独立编译)并以 .h
结尾。非头文件但用于被包含的文件应以 .inc 结尾,且应谨慎使用。
所有头文件应该是自包含的。用户和重构工具不应该需要遵守特殊条件才能包含该头文件。具体来说,头文件应该有头文件保护符并包含它需要的所有其他头文件。
当头文件声明了客户端将实例化的内联函数或模板时,内联函数和模板也必须在头文件中有定义,可以直接定义或通过它包含的文件。不要将这些定义移到单独包含的头文件(-inl.h)中;这种做法在过去很常见,但现在不再允许。当模板的所有实例化都发生在一个
.cc
文件中时,无论是因为它们是显式的 还是因为定义仅对该
.cc 文件可访问,模板定义都可以保留在该文件中。
有些罕见的情况下,设计用于被包含的文件不是自包含的。这些文件通常打算在不寻常的位置被包含,例如另一个文件的中间。它们可能不使用头文件保护符,也可能不包含它们的前置依赖。使用
.inc 扩展名命名这些文件。谨慎使用,尽可能优先使用自包含头文件。
#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_只包含你使用的(Include What You Use)
如果一个源文件或头文件引用了在其他地方定义的符号,该文件应该直接包含一个旨在提供该符号的声明或定义的头文件。不应出于任何其他原因包含头文件。
不要依赖传递性包含。这允许人们从头文件中移除不再需要的 #include
语句而不会破坏客户端。这也适用于相关头文件——如果 foo.cc 使用了 bar.h
中的符号,即使 foo.h 已包含了 bar.h,foo.cc 也应该包含 bar.h。
前置声明(Forward Declarations)
尽可能避免使用前置声明。 相反,请包含你需要的头文件。
“前置声明”是没有关联定义的实体声明。
// In a C++ source file:
class B;
void FuncInB();
extern int variable_in_b;
ABSL_DECLARE_FLAG(flag_in_b);- 前置声明可以节省编译时间,因为
#include会强制编译器打开更多文件并处理更多输入。 - 前置声明可以减少不必要的重新编译。由于头文件中不相关的更改,
#include可能会迫使你的代码更频繁地重新编译。
-
前置声明可以隐藏依赖关系,导致用户代码在头文件更改时跳过必要的重新编译。
-
与
#include语句相比,前置声明使自动化工具难以发现定义该符号的模块。 -
前置声明可能会被库的后续更改所破坏。函数和模板的前置声明可能会阻止头文件所有者对其 API 进行原本兼容的更改,例如拓宽参数类型、添加带默认值的模板参数或迁移到新的命名空间。
-
对命名空间
std::中的符号进行前置声明会导致未定义行为。 -
可能很难确定是需要前置声明还是完整的
#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*)。 -
对来自同一头文件的多个符号进行前置声明可能比简单地
#include该头文件更冗长。 -
为了支持前置声明而构造代码(例如,使用指针成员代替对象成员)可能使代码更慢和更复杂。
尽量避免对在其他项目中定义的实体进行前置声明。
在头文件中定义函数
仅当定义较短时,才在头文件的声明点包含函数的定义。如果定义因其他原因需要在头文件中,请将其放在文件的内部部分。如果需要使定义符合
ODR 安全,请使用 inline 说明符标记。
在头文件中定义的函数有时被称为”内联函数(Inline Functions)“,这是一个有些重载的术语,指的是几种不同但重叠的情况:
- 文本内联的符号定义在声明点对读者可见。
- 在头文件中定义的函数或变量是可内联展开的,因为其定义可用于编译器的内联展开 ,这可以生成更高效的目标代码。
- ODR 安全的实体不违反”单一定义规则(One Definition Rule)“ ,这通常要求在头文件中定义的事物使用 inline 关键字。
虽然函数往往是更常见的困惑来源,但这些定义也适用于变量,这里的规则同样如此。
- 文本内联定义函数可以减少简单函数(如访问器和修改器)的样板代码。
- 如上所述,头文件中的函数定义可以由于编译器的内联展开而为小函数生成更高效的目标代码。
- 函数模板和
constexpr函数通常需要在声明它们的头文件中定义(但不一定在公共部分)。
- 在公共 API 中嵌入函数定义会使 API 更难浏览,并给 API 的读者带来认知负担——函数越复杂,成本越高。
- 公共定义暴露了充其量无害、通常是多余的实现细节。
仅当函数较短(比如 10
行或更少)时,才在其公共声明处定义。除非出于性能或技术原因必须在头文件中,否则将较长的函数体放在
.cc 文件中。
即使定义必须在头文件中,这也不是将其放在公共部分的充分理由。相反,定义可以在头文件的内部部分,如类的
private 部分、包含 internal
一词的命名空间中,或在类似 // Implementation details only below here
的注释下方。
一旦定义在头文件中,它必须通过具有 inline
说明符或通过作为函数模板或在首次声明时在类体中定义而隐式指定为内联来保证
ODR 安全。
template <typename T>
class Foo {
public:
int bar() { return bar_; }
void MethodWithHugeBody();
private:
int bar_;
};
// Implementation details only below here
template <typename T>
void Foo<T>::MethodWithHugeBody() {
...
}包含文件的名称和顺序
按以下顺序包含头文件:相关头文件、C 系统头文件、C++ 标准库头文件、其他库的头文件、你项目的头文件。
项目的所有头文件都应列为项目源目录的后代,不使用 UNIX 目录别名
.(当前目录)或 ..(父目录)。例如,
google-awesome-project/src/base/logging.h 应按如下方式包含:
#include "base/logging.h"只有在库要求这样做时,才应使用尖括号路径包含头文件。特别是,以下头文件需要尖括号:
- C 和 C++ 标准库头文件(例如
<stdlib.h>和<string>)。 - POSIX、Linux 和 Windows 系统头文件(例如
<unistd.h>和<windows.h>)。 - 在极少数情况下,第三方库(例如
<Python.h>)。
在 dir/foo.cc 或 dir/foo_test.cc 中,如果其主要目的是实现或测试
dir2/foo2.h 中的内容,按以下顺序排列你的包含:
-
dir2/foo2.h。 -
一个空行
-
C 系统头文件,以及所有带
.h扩展名的尖括号头文件,例如<unistd.h>、<stdlib.h>、<Python.h>。 -
一个空行
-
C++ 标准库头文件(无文件扩展名),例如
<algorithm>,<cstddef>. -
一个空行
其他库的
.h文件。 一个空行 -
你项目的
.h文件。
用一个空行分隔每个非空的组。
使用上述推荐的顺序,如果相关头文件 dir2/foo2.h
遗漏了任何必要的包含,dir/foo.cc 或 dir/foo_test.cc
的构建将会失败。因此,这条规则确保构建失败首先出现在处理这些文件的人面前,而不是其他包中无辜的人。
dir/foo.cc 和 dir2/foo2.h
通常在同一目录中(例如,base/basictypes_test.cc 和
base/basictypes.h),但有时也可能在不同的目录中。
请注意,诸如 stddef.h 之类的 C 头文件与其 C++
对应物(cstddef)基本上是可以互换的。两种风格都可以接受,但优先与现有代码保持一致。
在每个部分中,包含应按字母顺序排列。请注意,较旧的代码可能不符合此规则,应在方便时修复。
例如,google-awesome-project/src/foo/internal/fooserver.cc
中的包含可能如下所示:
#include "foo/server/fooserver.h"
#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <vector>
#include "base/basictypes.h"
#include "foo/server/bar.h"
#include "third_party/absl/flags/flag.h"例外:
有时,特定于系统的代码需要条件包含。此类代码可以将条件包含放在其他包含之后。当然,保持特定于系统的代码小而局部化。示例:
#include "foo/public/fooserver.h"
#ifdef _WIN32
#include <windows.h>
#endif // _WIN32