一、预编译头文件:到底在加速什么?
先问大家一个问题:你有没有遇到过这种情况?
项目里某个头文件被几十个 .cpp 文件包含,每次改一行代码,整个项目要重新编译好几分钟。我早期做 C++ 项目时就经常被这个折磨,尤其是那些大型的第三方库头文件,比如 Boost、OpenCV 之类的。每次编译都像在等一杯咖啡,而且你还不能走开,因为编译完了可能还有下一个错误等着你。
嗯,预编译头文件(PCH)就是来解决这个问题的。
说白了,预编译头文件就是把那些「几乎不变」的头文件提前编译成二进制格式。这样编译器每次遇到这些头文件时,就不用重新解析了,直接加载编译好的结果。速度能快多少?我实测过,某些项目能提速 3-5 倍。
核心思想:把「稳定」的头文件编译一次,后面直接复用。
二、CMake 中的 target_precompile_headers
CMake 3.16 开始正式支持 target_precompile_headers 这个命令。在这之前,大家要么手动配置,要么用第三方工具。我个人觉得,CMake 官方支持之后,这件事变得简单多了。
基本语法
target_precompile_headers(<target>
<PRIVATE|PUBLIC|INTERFACE>
<header1> [<header2> ...]
)
这里要注意三个关键字:
- PRIVATE:只对当前目标生效,不传递给依赖它的目标
- PUBLIC:当前目标和依赖它的目标都会使用
- INTERFACE:当前目标不用,但依赖它的目标会用
我个人习惯用 PRIVATE,因为预编译头文件通常是实现细节,不应该暴露给外部。
一个完整的例子
cmake_minimum_required(VERSION 3.16)
project(PCHDemo)
add_executable(myapp main.cpp utils.cpp)
target_precompile_headers(myapp PRIVATE
<vector>
<string>
<iostream>
"my_common.h"
)
这里我把 vector、string、iostream 这些标准库头文件,以及项目自己的 my_common.h 都放进了预编译头文件。注意看,标准库头文件用尖括号,项目头文件用双引号,这个细节别搞混了。
三、避坑指南:我曾经踩过的坑
预编译头文件虽然好用,但坑也不少。我一个个说。
坑1:头文件顺序很重要
我曾经在一个项目里把 <windows.h> 和 <winsock2.h> 同时放进预编译头文件,结果编译报了一堆莫名其妙的错误。后来发现,这两个头文件有依赖顺序,必须先包含 <winsock2.h> 再包含 <windows.h>。
注意:预编译头文件中的头文件顺序,必须和实际代码中的包含顺序一致。否则编译器会报错。
坑2:不要放频繁修改的头文件
预编译头文件一旦修改,所有依赖它的源文件都要重新编译。所以只放那些「几乎不变」的头文件。我见过有人把业务逻辑的头文件也放进去,结果每次改代码都要重新编译整个项目,反而更慢了。
坑3:跨平台兼容性
不同编译器对预编译头文件的支持不一样。MSVC 和 GCC 都支持,但 Clang 在有些版本上支持得不太好。如果你做跨平台项目,建议先测试一下。
四、知识体系:一张图看懂
下面这张图展示了预编译头文件的完整工作流程和关键决策点:
五、进阶用法:批量添加头文件
如果你的项目有很多头文件要加入预编译,一个个写太累了。可以用 file(GLOB) 配合循环来处理:
# 收集所有公共头文件
file(GLOB COMMON_HEADERS
${CMAKE_SOURCE_DIR}/include/common/*.h
${CMAKE_SOURCE_DIR}/include/thirdparty/*.h
)
# 添加到预编译头文件
target_precompile_headers(myapp PRIVATE ${COMMON_HEADERS})
不过要注意,file(GLOB) 不会自动检测新增文件。如果你添加了新头文件,需要重新运行 CMake 配置。我个人建议在 CMakeLists.txt 里加个注释提醒自己。
六、性能对比:到底能快多少?
我拿一个实际项目做了测试,项目大概有 200 个 .cpp 文件,包含大量 STL 和 Boost 头文件。结果如下:
| 编译方式 | 首次编译 | 增量编译(改一个 .cpp) | 增量编译(改头文件) |
|---|---|---|---|
| 不使用 PCH | 120 秒 | 15 秒 | 30 秒 |
| 使用 PCH | 130 秒(首次稍慢) | 5 秒 | 25 秒 |
可以看到,首次编译因为要生成 PCH 文件,反而慢了 10 秒。但增量编译快了 3 倍。如果你每天要编译几十次,这个收益非常可观。
小技巧:如果项目很大,可以考虑把 PCH 生成和主项目分开编译。这样改代码时不会触发 PCH 重新生成。
七、总结
预编译头文件是个很实用的优化手段,尤其适合那些头文件多、编译慢的大型项目。但要注意:
- 只放稳定的头文件
- 注意头文件顺序
- 跨平台时先测试
- 首次编译会稍慢,但增量编译收益很大
我个人建议,如果你的项目编译时间超过 30 秒,就值得考虑用预编译头文件了。嗯,今天就聊到这里,希望对你有帮助。