通常每个 .cc
文件应该有一个配套的 .h
文件. 单元测试和仅有 main()
的 .cc
文件是例外。
1.1、使用头文件的好处:
1. 自给自足的头文件:头文件应独立完整,包含所需的所有其他头文件和定义,无需特殊前提条件。
2. 头文件防护符:头文件应使用 `#define` 防护符来防止重复导入。
3. 内联函数和模板的实现:如果头文件声明了内联函数或模板,并且使用者需要实例化这些组件,头文件应提供相应的实现。不应将实现放在另一个头文件(如 `-inl.h`)中,这种做法已被禁止。
4. 显式实例化:如果模板的所有实例化都在一个 `.cc` 文件中,可以将模板定义放在该 `.cc` 文件中。
5. 特殊情况下的文件:有些用于导入文件可能不自给自足,通常用于特殊导入位置,这类文件不需要头文件防护符和导入依赖,应使用 `.inc` 扩展名,并尽量减少使用。
6. 推荐做法:在可行的情况下,应优先使用自给自足的头文件。
1.2、#define 防护符
所有头文件应使用 #define
防护符防止重复导入,格式为 项目_路径_文件名_H_
。
例如, foo
项目中的文件 foo/src/bar/baz.h
应该有如下防护:
#ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ ... #endif // FOO_BAR_BAZ_H_
1.3. 导入你的依赖
1. 直接导入:如果代码文件或头文件引用了其他位置定义的符号,应直接导入包含该符号声明或定义的头文件。
2. 避免间接导入:不应依赖于其他头文件间接提供的符号,以防止删除不再需要的 `#include` 语句时影响代码的使用者。
3. 适用性:此规则适用于所有情况,包括配套文件。例如,如果 `foo.cc` 使用了 `bar.h` 中的符号,即使 `foo.h` 已经包含了 `bar.h`,`foo.cc` 也应直接导入 `bar.h`。
1.4. 前向声明
1. 避免前向声明:推荐直接导入所需的头文件,而不是使用前向声明。
2. 前向声明定义:前向声明是无对应定义的声明,例如类声明、函数声明等。
3. 优点:
- 节省编译时间,因为#include会使编译器打开更多文件。
- 避免因使用#include,导致头文件中无关改动引发的重复编译。
4. 缺点:
- 隐藏依赖关系,可能导致忽略头文件变化后必要的重新编译。
- 相比 #include
, 前向声明的存在会使自动化工具难以发现定义符号的模块。
- 可能阻碍库的修改,如API变更、参数类型拓宽等。
- 为 std:: 命名空间的符号前向声明可能导致未定义行为。
- 难以判断何时使用前向声明,可能改变代码含义。
-
// 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); } // 调用 f(B*)
若用
B
和D
的前向声明替代#include
,test()
会调用f(void*)
.
- 替代 `#include` 可能导致调用错误的函数重载。
- 为多个符号前向声明可能比直接 `#include` 更冗长。
- 为兼容前向声明设计的代码可能更慢更复杂。
5. 结论:应尽量避免使用前向声明,尤其是在其他项目定义的实体上。
1.5. 内联函数
1. 内联函数定义:内联函数是建议编译器展开而不是使用正常函数调用的函数。
2. 内联函数建议:只对10行以下的小函数使用内联。
3. 优点:
- 内联小函数可以提高目标代码效率。
- 推荐对存取函数、变异函数和其他短小且关键性能的函数使用内联。
4. 缺点:
- 滥用内联可能导致程序变慢。
- 内联可能增加或减少代码体积,但大函数内联会增加代码大小。
5. 结论:
- 不要内联超过10行的函数。
- 析构函数可能比看起来更长,因为它们可能隐式调用成员和基类的析构函数。
- 内联含循环或 `switch` 语句的函数通常不划算,除非这些结构很少执行。
6. 其他注意事项:
- 即使声明为内联,编译器也可能不执行内联,特别是虚函数和递归函数。
- 递归函数通常不应声明为内联,因为编译器通常不支持。
- 虚函数内联的主要目的是为了类内定义和注释行为,适用于存取和变异函数。
1.6. #include
的路径及顺序
1. 头文件导入顺序:推荐按照以下顺序导入头文件:
- 配套的头文件
- C 语言系统库头文件
- C++ 标准库头文件
- 其他库的头文件
- 本项目的头文件
2. 头文件路径规范:头文件路径应相对于项目源码目录,避免使用 `.` 或 `..`。
3. 导入示例:例如,导入 `google-awesome-project/src/base/logging.h` 应使用 `#include "base/logging.h"`。
4. 文件实现或测试:在 `dir/foo.cc` 或 `dir/foo_test.cc` 中导入 `dir2/foo2.h` 时,应遵循上述顺序,并在每个非空分组之间用空行隔开。
5. 构建失败提示:这种顺序确保了如果 `dir2/foo2.h` 缺少必要导入,构建 `dir/foo.cc` 或 `dir/foo_test.cc` 会失败,从而让维护者及时发现问题。
6. 头文件位置:`dir/foo.cc` 和 `dir2/foo2.h` 通常位于同一目录,但也可能不同。
7. C/C++ 头文件等效性:C 语言头文件(如 `stddef.h`)和对应的 C++ 头文件(如 `cstddef`)是等效的,应与现有代码风格保持一致。
8. 导入语句排序:每个分组内部的导入语句应按字母序排列,旧代码应适时修正。
9. 导入语句示例:给出了 `google-awesome-project/src/foo/internal/fooserver.cc` 的导入语句顺序。
10. 平台相关代码:平台相关的代码可能需要有条件地导入,这应在其他导入语句后进行,尽量保持简洁且影响范围小。
11. 条件导入示例:给出了基于 `LANG_CXX11` 条件导入 `initializer_list` 的示例。
1.7后记
有人提出库文件放在最后, 这样出错先是项目内的文件, 头文件都放在对应源文件的最前面, 这一点足以保证内部错误的及时发现了.
类内部的函数一般会自动内联。所以某函数一旦不需要内联,其定义就不要再放在头文件里,而是放到对应的 .cc
文件里。这样可以保持头文件的类相当精炼,也很好地贯彻了声明与定义分离的原则。