当前位置:首页 > 博客 > 笔记 > 正文内容

Header Files 头文件

RWYQ阿伟2025-12-22笔记820

头文件

        通常情况下,每一个.cc文件都应该有一个对应的.h文件。当然也有常见的例外情况,例如单元测试,或者仅包含一个main()函数的小型.cc文件。

        正确使用头文件(header files)会对您代码的可读性、体积以及性能产生巨大的影响。

        以下的规则将引导您避开使用头文件时的各种陷阱。

自包含的头文件

        头文件应该是自包含的(能够独立编译),且以 .h 为后缀。那些旨在用于包含的非头文件,应以 .inc 为后缀,并且应当谨慎使用。

        所有的头文件都应该是自包含的。用户和重构工具在包含该头文件时,不应必须遵守任何特殊条件。具体而言,一个头文件应当具备头文件防护(Header Guards),并包含其所需的所有其他头文件。

        当一个头文件声明了内联函数(inline functions)或模板(templates),且该头文件的使用者(clients)需要对其进行实例化(instantiate)时,这些内联函数和模板必须在头文件中提供定义(definition),这些定义可以直接写在头文件中,或者放在该头文件所包含的其他文件中。请不要将这些定义移至单独包含的头文件(例如 -inl.h 文件)中;这种做法在过去很常见,但现在已不再允许。当一个模板的所有实例化都发生在一个 .cc 文件中时(无论是因为进行了显式实例化,还是因为定义仅对该 .cc 文件可见),此时可以将该模板的定义保留在该 .cc 文件中。

        存在极少数情况,那些设计用来被包含的文件并不是自包含的。这些文件通常旨在被包含在一些不寻常的位置,例如另一个文件的中间。它们可能不使用头文件防护(header guards),也可能不包含其先决条件(prerequisites)。请使用 .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_

只包含你用到的文件

        如果一个源文件或头文件引用了在别处定义的符号(symbol),该文件应该直接包含一个能够正确提供该符号声明或定义的头文件。不应为了其他任何原因包含头文件。

        不要依赖传递性包含(transitive inclusions)。 这样做允许人们在移除头文件中不再需要的 #include 语句时,不会破坏依赖该头文件的客户端代码。这条规则同样适用于相关的头文件——即使 foo.h 已经包含了 bar.h,如果 foo.cc 直接使用了 bar.h 中的符号,那么 foo.cc 也应该显式地包含 bar.h。

前置声明

        尽可能避免使用前向声明。相反,应直接包含所需的头文件。

        定义:“前向声明”是指对一个实体的声明,但不包含其相关联的定义。

// 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 引入头文件,对头文件中的多个符号进行前向声明可能会导致代码更加冗长。

  • 增加复杂性与降低性能:为了能够使用前向声明而对代码进行结构调整(例如,使用指针成员代替对象成员),可能会使代码更加复杂,并导致运行速度变慢。

结论:

        尽量避免对其他项目中定义的实体进行前向声明。

在头文件中定义函数

        只有当函数定义很短小时,才应在头文件中的声明处直接包含其定义。如果该定义必须放在头文件中,应将其置于文件的内部区域(如匿名命名空间或 detail 命名空间)。如果需要确保定义满足 ODR(单一定义规则)安全,请使用 inline 说明符对其进行标记。

        定义:在头文件中定义的函数有时被称为“内联函数”。这是一个含义有些复杂的术语,它指代了几种不同但又相互重叠的情况:

  1. 文本内联:符号的定义在声明处即对阅读者可见。

  2. 可展开内联:由于编译器可以获取到函数的定义,因此可以对其进行内联展开,从而生成更高效的机器码。

  3. ODR 安全:实体不会违反“单一定义规则”。在头文件中定义内容时,通常需要使用 inline 关键字来保证这一点。

        虽然函数更容易引起混淆,但这些定义同样适用于变量,本节的规则也是如此。

优点:

  • 对于访问器(accessors)和修改器(mutators)等简单函数,将其定义直接写在声明处可以减少样板代码。

  • 如上所述,对于小型函数,由于编译器会进行内联展开,将其定义放在头文件中通常可以生成更高效的机器码。

  • 函数模板和 constexpr 函数通常需要在声明它们的头文件中进行定义(但不一定非要在公共接口部分)。

缺点:

  • 干扰 API 可读性:将函数定义嵌入到公共 API 中,会使 API 的概览变得困难,并给阅读者带来认知负担——函数越复杂,这种代价就越高。

  • 暴露无关实现细节:公共定义暴露了实现细节,这些细节往往不仅毫无益处,甚至可能是多余的。

结论:

  • 仅在函数体很短时(例如 10 行代码或更少)才在公共声明处进行定义。 除非出于性能或技术上的硬性要求,较长的函数体应放在 .cc 文件中。

  • 即便某个定义必须放在头文件中,这也不足以成为将其放入公共部分的理由。相反,该定义应置于头文件的内部区域,例如类的 private 私有部分、包含 internal 字样的命名空间中,或者放在类似 // 以下仅为实现细节 这类注释的下方。

  • 一旦定义位于头文件中,必须通过显式添加 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 中的内容,请按以下顺序包含头文件:

  1. dir2/foo2.h。

  2. 空行

  3. C 系统头文件,以及任何其他带 .h 扩展名的尖括号头文件,例如:<unistd.h>、<stdlib.h>、<Python.h>。

  4. 空行

  5. C++ 标准库头文件(无文件扩展名),例如:<algorithm>、<cstddef>。

  6. 空行

  7. 其他库的 .h 文件。

  8. 空行

  9. 你的项目 .h 文件。

        使用一个空行来分隔每一个非空的分组。

        根据这种首选的顺序,如果相关的头文件 dir2/foo2.h 遗漏了任何必要的包含项,dir/foo.cc 或 dir/foo_test.cc 的构建将会失败。因此,这条规则确保了构建错误首先会出现在正在处理这些文件的开发人员面前,而不是出现在其他无关的、其他软件包中的开发人员面前。

        dir/foo.cc 和 dir2/foo2.h 通常位于同一个目录下(例如 base/basictypes_test.cc 和 base/basictypes.h),但有时也可能位于不同的目录中。

        请注意,C 头文件(例如 stddef.h)与其 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


扫描二维码推送至手机访问。

版权声明:本文由阿伟的笔记本发布,如需转载请注明出处。

本文链接:http://awnotebook.com/post/923.html

标签: 笔记编程C++
分享给朋友:

发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。