Skip to content

头文件

多文件 - C++类 & 模板

Abstract

说法 对错
每个 .cpp 都必须有同名 .h
每个 .cppinclude/ 里都要有一份头
头文件用于在多个翻译单元间共享声明
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 提供对应定义。


工程实践

  1. 默认少暴露:能放在单个 .cpp 里的 helper 不要进公共头。

  2. 公共头保持精简:只放用户真正需要的类型与函数;大实现放 .cpp 或 PIMPL。

  3. 内部头放 src/:命名如 *_impl.hpp*_detail.hpp,不安装到 include/

  4. 避免在头文件里 #include 过多:能用前置声明(forward declaration)则减少依赖,加快编译。

  5. 测试若需访问内部:优先测公共行为;确需白盒时再用测试友元、或把测试编进同一 static 库并共享 src 私有头(教学/小项目可接受,不必默认)。