头文件¶
Abstract
| 说法 | 对错 |
|---|---|
每个 .cpp 都必须有同名 .h | ❌ |
每个 .cpp 在 include/ 里都要有一份头 | ❌ |
| 头文件用于在多个翻译单元间共享声明 | ✅ |
include/ 只放对外公共 API,src/ 可放实现与私有头 | ✅(推荐约定,非语言强制) |
判断标准:有没有其他翻译单元(或其他库的使用方)需要 #include 这些声明——有则写头文件(并区分是否放进 include/);没有则写在单个 .cpp 里即可。
头文件与多文件工程¶
C++ 把每个经预处理后能独立编译的源文件称为一个翻译单元(translation unit),通常对应一个 .cpp / .cc 文件及其 #include 展开后的全部内容。链接阶段再把多个翻译单元拼成可执行文件或库。
项目变大后,很少把所有代码写进一个文件,而是拆成多文件工程,常见目录约定例如:
include/ # 对外安装的公共 API(可选)
src/ # 实现与内部头文件
头文件(.h / .hpp)在多文件工程里的核心作用,是在多个翻译单元之间共享声明(类型、函数、常量、inline 函数等),并配合头文件保护(#pragma once 或 include guard)避免重复定义。实现可以放在 .cpp 里,也可以对头文件中的 inline / 模板代码直接写在头文件中。
需要区分的三个概念:
| 概念 | 含义 |
|---|---|
| 声明 vs 定义 | 头文件里多为声明;非 inline 的函数/变量定义通常只应出现在一个 .cpp 中(或显式 inline/constexpr 规则允许处) |
| 公共 API vs 实现细节 | 给库使用者看的接口 ≠ 每个 .cpp 里的内部辅助 |
「有头文件」≠「头文件必须在 include/」 | 仅本模块使用的头可以放在 src/,不必进入对外安装目录 |
类与模板在多文件中的基础分工,见 多文件(.h & .cpp)。
include/ 与 src/ 的对应关系¶
很多 CMake / 库项目会区分:
| 位置 | 角色 | 典型消费者 |
|---|---|---|
include/...(如 include/mylib/foo.hpp) | 公共 API:稳定、可安装、文档化 | 依赖该库的其他 target、examples/ |
src/... 旁或 src/.../detail/ | 私有 / 实现头:类成员布局、模块内细节 | 仅本库的 .cpp 与同模块内部 |
因此:
-
不是「每个
src/**/*.cc都在include/里有一份同名头文件」。 -
而是「只有需要被库外代码
#include的声明才放进include/」。
示例:
include/mylib/stack.hpp ← 用户只 include 这个
src/stack/stack.cpp ← Stack 成员函数实现
src/stack/nic.cpp ← 可能只 include 内部头 src/stack/nic.hpp
src/stack/nic.hpp ← 不进入 include/,不作为稳定 ABI
.cpp 可以只 include 公共头(小模块),也可以 公共头 + 私有头(大模块常见)。
CMake 中常配合:target_include_directories(mylib PUBLIC include),对 src 使用 PRIVATE 包含路径,使外部 target 无法误 include 实现细节。
不是每个 .cpp 都需要配套头文件¶
语言层面没有「有 .cpp 就必须有 .h」的规定。是否需要头文件,取决于是否有别的翻译单元需要看到这些声明。
可以只有 .cpp、不必单独 .h 的情况¶
| 情况 | 说明 |
|---|---|
| 程序入口 | main.cpp 通常没有同名头文件 |
匿名命名空间 / 文件内 static | 符号仅在本翻译单元可见 |
仅在本 .cpp 使用的类型与函数 | 实现细节,无需共享 |
| 显式模板实例化 | 实例化写在 .cpp,模板定义可在别处头文件中 |
| 测试辅助、小工具 | 单文件自洽即可 |
编译器按翻译单元工作:该单元内声明齐全即可编译;链接时再解析跨文件的符号。
什么时候才需要头文件¶
-
多个
.cpp要共用同一套声明(类、非内联函数原型、常量、inline函数等)。 -
要提供库的稳定公共接口给外部或上层模块。
-
有意分离接口与实现,减少修改实现时的重编译范围,或隐藏实现(PIMPL 等)。
「每个类一个
.h+.cpp」是常见工程风格,不是 C++ 语法要求。
与编译、链接的关系¶
foo.hpp ──#include──► a.cpp ──编译──► a.o
└──► b.cpp ──编译──► b.o ──链接──► 可执行文件 / libfoo.a
-
头文件:在预处理阶段被文本展开进翻译单元。
-
ODR(One Definition Rule):类型与函数在整个程序中的定义通常只能有一处(
inline/ 模板等例外见标准与 C++类 & 模板)。 -
链接错误(undefined reference)常表示:某
.cpp用了声明,但没有任何.o提供对应定义。
工程实践¶
-
默认少暴露:能放在单个
.cpp里的 helper 不要进公共头。 -
公共头保持精简:只放用户真正需要的类型与函数;大实现放
.cpp或 PIMPL。 -
内部头放
src/:命名如*_impl.hpp、*_detail.hpp,不安装到include/。 -
避免在头文件里
#include过多:能用前置声明(forward declaration)则减少依赖,加快编译。 -
测试若需访问内部:优先测公共行为;确需白盒时再用测试友元、或把测试编进同一 static 库并共享
src私有头(教学/小项目可接受,不必默认)。