很多人装好 Clang 后,第一反应是“怎么把预处理结果单独导出来”,但真正上手时又常被 C/C++ 模式、头文件路径和输出方式这些细节卡住。本文把 clang -E 和 clang++ -E 的用法拆开讲清楚,再补上常见报错、-I/-D 的处理方式,以及它和 -save-temps 的区别,方便你判断什么时候该看 .i/.ii,什么时候别指望预处理文件回答语义问题。
clang -E 到底会做什么
clang -E 做的只是预处理这一步:把头文件展开、宏替换、注释移除,然后输出纯文本结果。它不会进入真正的编译阶段,也不会生成目标文件或可执行文件。
生成的文件通常是:
- C 源文件对应
.i - C++ 源文件更常见的是
.ii
这些文件本质上就是预处理后的源码文本,可以直接用 cat 或编辑器打开查看。
C 和 C++ 的输出区别
关键不在于扩展名本身能不能改,而在于语言模式是否正确:
- C 文件通常用
clang -E,输出成.i - C++ 文件更推荐用
clang++ -E,输出成.ii
虽然 C++ 预处理结果也可以手动命名成 .i,但 Clang 默认还是倾向用 .ii 表示 C++ 的预处理输出。更重要的是,clang++ 会自动按 C++ 模式处理,能避开语言模式判断错误带来的问题。
怎么生成 .i 或 .ii 文件
只想拿到预处理结果时,命令本身很直接:

clang -E main.c -o main.i
clang++ -E main.cpp -o main.ii
这里有几个实用细节:
main.c用clang -E main.c -o main.imain.cpp推荐用clang++ -E main.cpp -o main.ii- 不加
-o时,结果会直接输出到stdout - 预处理后文件体积常常会暴涨十倍以上,因为所有
#include内容都会被展开进来
不带 -o 虽然也能跑,但结果直接冲到终端或管道里,既不方便查看,也容易因为输出过长而处理混乱。实际使用时,单独落成文件通常更稳妥。
常见报错通常出在哪
如果你发现预处理失败,问题往往不在 -E 本身,而在语言模式、头文件搜索路径或宏参数没带全。

C++ 头文件报错时,先检查是不是用了 clang
clang -E 默认按 C 语言处理。遇到 C++ 源码里的 #include <iostream>,就可能直接报错找不到头文件。这种情况首先应改用:
clang++ -E main.cpp -o main.ii
原因不是 -E 失效,而是你前面就进错了语言模式。
头文件路径不对时,用 -I 补搜索路径
系统头文件路径没有自动带上,或者自定义头文件放在子目录时,就需要显式加 -I:
clang++ -E -I/usr/include/c++/13 main.cpp -o main.ii
clang++ -E -I./include main.cpp -o main.ii
前者适合系统 C++ 头文件路径没被正确识别的情况,后者适合项目内头文件放在 include 子目录的场景。另一个容易忽略的点是当前工作目录;路径写对了,但执行目录不对,结果也一样会报找不到文件。
宏没有生效时,检查 -D 是否遗漏
如果你的目标是验证某个条件编译或宏替换结果,别忘了把宏定义一并传进去:
clang++ -E -DMY_DEBUG=1 main.cpp
很多“宏没展开”的问题,最后都只是因为预处理时漏传了 -D。
为什么通常不建议用 -save-temps 代替 -E
-save-temps 确实也会生成预处理文件,但它并不会停在这一步,而是继续往后走完编译、汇编、链接流程,并顺手留下更多中间产物。

两者的差别可以直接看结果:
clang++ -save-temps main.cpp
会生成:
main.cpp.imain.cpp.smain.cpp.oa.out
clang++ -E main.cpp -o main.ii
只会生成:
main.ii
如果你的目的只是检查宏展开、条件编译结果,或者查看头文件最终被拼成了什么样,-E 更干净、更快,也更可控。调试时还可以在输出里搜索 # 1 "main.cpp" 这样的行号标记,快速回到原始文件位置。
为什么 .ii 里看不到你以为会“展开”的内容
很多人第一次打开 main.ii,会期待看到 std::string 被“彻底展开”,但这本来就不是预处理阶段的职责。
预处理只负责文本层面的工作,不负责语义分析。也就是说:
- 你能看到
#include <string>展开后的大量文本,可能是几千行 - 你可能会看到
std::basic_string<char>这样的形式 - 但你看不到
std::string成员函数的真正实现过程
那些内容属于后续的 AST、IR、编译优化等阶段,通常要到 -S 或 -c 相关流程里才会体现出来。
看 .ii 文件时,应该重点找什么
如果你是为了排查问题,阅读 .ii/.i 时更有效的做法通常是:
- 检查某个
#include是否真的被展开进来了 - 搜索宏替换后的字面量结果,比如
"Hello world" - 结合行号标记定位原始源码对应位置
相比之下,试图在预处理文件里寻找模板实例化、类型推导或标准库语义展开,往往方向就错了。
什么时候该看预处理文件
预处理文件看起来只是“一份展开后的纯文本”,但在排查宏逻辑、条件编译和头文件依赖时,它往往是最接近真实输入的依据。你可以把它理解成编译前的完整记录:它非常适合回答“编译器最终拿到了什么文本”,但并不负责回答“这些文本在语义上会如何被解释”。









