一、预编译头文件:到底在加速什么?

先问大家一个问题:你有没有遇到过这种情况?

项目里某个头文件被几十个 .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"
)

这里我把 vectorstringiostream 这些标准库头文件,以及项目自己的 my_common.h 都放进了预编译头文件。注意看,标准库头文件用尖括号,项目头文件用双引号,这个细节别搞混了。

三、避坑指南:我曾经踩过的坑

预编译头文件虽然好用,但坑也不少。我一个个说。

坑1:头文件顺序很重要

我曾经在一个项目里把 <windows.h><winsock2.h> 同时放进预编译头文件,结果编译报了一堆莫名其妙的错误。后来发现,这两个头文件有依赖顺序,必须先包含 <winsock2.h> 再包含 <windows.h>

注意:预编译头文件中的头文件顺序,必须和实际代码中的包含顺序一致。否则编译器会报错。

坑2:不要放频繁修改的头文件

预编译头文件一旦修改,所有依赖它的源文件都要重新编译。所以只放那些「几乎不变」的头文件。我见过有人把业务逻辑的头文件也放进去,结果每次改代码都要重新编译整个项目,反而更慢了。

坑3:跨平台兼容性

不同编译器对预编译头文件的支持不一样。MSVC 和 GCC 都支持,但 Clang 在有些版本上支持得不太好。如果你做跨平台项目,建议先测试一下。

四、知识体系:一张图看懂

下面这张图展示了预编译头文件的完整工作流程和关键决策点:

预编译头文件工作流程 源文件 .cpp 使用PCH? 预编译头文件 .pch (已编译的二进制) 编译器 传统编译 (逐行解析头文件) 编译结果 (速度慢) 关键决策点 ✅ 放什么? 标准库头文件、第三方库头文件、项目公共头文件(几乎不变) ❌ 不放什么? 频繁修改的业务头文件、模板特化、平台差异大的头文件

五、进阶用法:批量添加头文件

如果你的项目有很多头文件要加入预编译,一个个写太累了。可以用 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 秒,就值得考虑用预编译头文件了。嗯,今天就聊到这里,希望对你有帮助。

公众号:蓝海资料掘金营,微信 deep3321