预编译头文件的作用是将大量稳定的头文件提前编译成二进制缓存。这样编译器在处理每个源文件时,直接读取该缓存,而无需重新解析成千上万行重复的代码,从而极大地缩短编译时间。
比如常用的标准模版库:比如向量、字符串、标准输出之类,假设我们每次(在每个
.cpp文件)都要包含vector,需要读取vector.h头文件并编译,而且vector本身也包含其他一系列头文件,所有其他头文件被复制到vector.h中,再把vector.h复制到.cpp中(大约十万行代码),还要进行解析和标记化,并以某种形式进行编译
- 如果项目中有多个cpp文件,很多地方都包括
vector.h,他们会被逐个包含在每个文件中每个翻译单元(.cpp)都会被单独编译,然后链接器将它们链接起来 - 且每次对那个cpp文件
包含了vector.h的重新更改时,整个这里是说要把vector.h复制进来然后再编译都得重新编译
建议
- 改用预编译头文件,它会获取一堆头文件(要包含的那些),然后它编译一次,并将起转换为二进制格式。这对编译器来说处理起来比纯文件快得多
因为它以处于解析状态,而且是二进制文件,随时可以,不用再重新编译- 举个例子,可能本来要1分钟,使用预编译头文件后,编译可能只需8秒钟
- 预编译头文件其实就是一个头文件,且包含一系列其他头文件
- 不可以把所有头文件都放进去,因为如果一个头文件经常变化,那么每次改动都需要重新编译整个预编译头文件
- 不需要在项目的每个cpp都包含日志文件,只需要包含含有日志的预编译头文件
- C库、标准模板库、WindowsAPI调用库,建议把这些都放进预编译头文件
每次编译可能他的代码量比实际代码还多。这样就能从每个C文件中获取所有内容,如果要用只能指针,就不用实际直接包含memory头文件,因为他在预编译头文件中,它会包含在实际编写的每个C++文件中 - 因为把所有文件都放进了
pch.h也可以叫其他名字,所以不能一眼看出实际include了什么,且pch.h中包含了所有的,所以也不清楚实际它只依赖哪些头文件 - 如果编写游戏引擎,可能只有一个cpp文件需要包含GLFW
编译一起就搞定,那么可以把GLFW放入pch.h中,因为其他内容都被抽象放在自己的api中;而其他的标准库、vector、标准输入输出之类的,很多都会用的,也可以让它编译一次就搞定
例子#
//Main.cpp
#include "pch.h"
int main()
{
std::cout << "Hello World!" << std::endl;
}//pch.h
#pragma once
#include <iostream>
#include <algorithm>
#include <functional>
#include <memory>
#include <thread>
#include <utility>
// Data structures
#include <string>
#include <stack>
#include <deque>
#include <array>
#include <vector>
#include <set>
#include <map>
#include <unordered_set>
#include <unordered_map>
//Windows API
#include <windows.h>做个测试

然后ctrl+f7编译Main.cpp,查看生成的Main.i文件

- 由于pch.h中的源代码全都被拷贝到了Main.i中,所以该文件有40几万行代码
- 每有cpp文件包含pch.h时,每次都会编译一次成这样的40几万行代码。使用预编译头文件避免这种情况
使用预编译头文件#
目前文件夹树图#

两个文件
把上面的预处理到文件关闭 很重要!

改进#
pch.cpp#
添加一个文件pch.cpp
//pch.cpp
#include "pch.h"右键pch.cpp文件,属性(properties)

当你设置 pch.cpp 为 Create (/Yc) 时,编译器会扫描这个源文件。它在寻找哪一行代码呢?就是你在框里填的那个名字。
- 触发开关:编译器一旦在 pch.cpp 中读到
#include "pch.h",它就会意识到:“这就是我要预编译的终点!”然后它把这一行之前的所有内容全部打包成二进制 .pch 文件。如果在这一行之后还有代码(虽然 Cherno 建议不要写),那些代码不会进入 PCH 缓存,而是会被当成普通的 C++ 代码编译进 pch.obj。 - 留空的后果:如果你留空,编译器就不知道该把哪个头文件作为预编译的“分界线”,测试后发现会把所有的内容全部打包进二进制 .pch 文件
验证#
假设有文件pch.h和pch.cpp
//文件名------pch.h
#pragma once
#include <iostream>//文件名------pch.cpp
//sdkfaksjdfk
#include "pch.h"
//Windows API
#include <windows.h>- 设置precompiledHeaderFile为空时,ctrl+f7编译pch.cpp后,HelloWorld72.pch大小为61312KB
- 设置precompiledHeaderFile为
pch.h时,ctrl+f7编译pch.cpp后,HelloWorld72.pch大小为42112KB
注意#
注意:这里一定要确保这个Preprocess to a file是No

项目属性#
确定后,右键项目-属性

含义:编译器在编译其他源文件时,一看到
#include "你填在框里的名字"。动作:它会直接停下读取文本的动作。
结果:它不去解析这行代码,而是直接去硬盘里加载已经做好的二进制 .pch 文件。
当然,之后还会继续读取下一个#include
完成!现在查看Main.cpp,会发现

使用了预编译头文件
现在RebuildSolution#

查看生成的文件

对比是否使用预编译头文件#
visual studio中#
开启编译计时
Tools-Options

先清理项目,然后进行构建
不使用预编译头文件#
项目属性–不使用预编译头文件

清理项目#
10:40:53:219 ========== Clean completed at 10:40 and took 02.234 seconds ==========
编译项目#
10:37:16:501 ========== Build completed at 10:37 and took 28.160 seconds ========== 花了28秒
修改后编译项目#
修改Main.cpp并添加两行std::cout输出后再次构建========== Build completed at 10:39 and took 12.515 seconds ========== 花了12秒
使用预编译头文件#

清理项目#
========== Clean completed at 10:43 and took 01.506 seconds ==========
编译项目#
========== Build completed at 10:43 and took 14.012 seconds ==========
修改后编译项目#
10:44:04:656 ========== Build completed at 10:44 and took 04.586 seconds ==========
g++中#
不使用预编译头文件#
/e/cppStudyTemp/ChernoCpp/HelloWorld72/HelloWorld72/src (main)
λ time g++ -std=c++11 Main.cpp
real 0m6.691s
user 0m0.047s
sys 0m0.141s使用预编译头文件#
先编译pch.h
/e/cppStudyTemp/ChernoCpp/HelloWorld72/HelloWorld72/src (main)
λ g++ -std=c++11 pch.h
//文件夹里会出现一个 pch.h.gch。这个 .gch 就是 g++ 版的 .pch 缓存。
会在文件夹生成一个pch.h.gch的文件

修改Main.cpp后重新编译
/e/cppStudyTemp/ChernoCpp/HelloWorld72/HelloWorld72/src (main)
λ time g++ -std=c++11 Main.cpp
//g++ 在编译 Main.cpp 时,看到 #include "pch.h",会自动在同目录下搜索是否存在 pch.h.gch。如果找到了,它就会像 VS 一样直接加载二进制快照。
real 0m1.996s
user 0m0.031s
sys 0m0.140s两者差异#
在使用 g++(或 Clang)的手动命令行模式下,确实不需要额外创建 pch.cpp 文件。
这是因为 Visual Studio (MSVC) 和 g++ 对待预编译头的逻辑有着本质的区别。
为什么 g++ 不需要 pch.cpp?
- g++ 的逻辑(直接编译头文件): g++ 允许你直接把 .h 文件当作编译目标。当你运行 g++ pch.h 时,它会直接生成一个名为 pch.h.gch 的二进制文件。
- MSVC 的逻辑(必须通过源文件触发): MSVC 规定所有编译动作必须由 .cpp 文件触发。它不具备直接“编译”一个 .h 文件的功能,所以必须借用一个只有一行代码的 pch.cpp 来告诉编译器:“以此为契机,把包含的头文件打包”。
虽然 g++ 看起来更简单,但有一个致命细节:
- 编译 pch.h 时的参数,必须与编译 Main.cpp 时完全一致。
- 如果你编译 pch.h 时用了 -std=c11,但编译 Main.cpp 时没加这个参数,g 会因为“环境不匹配”而默默无视掉你的预编译头,重新去读文本。你会发现编译速度一点没变,且没有任何报错提醒。