最直接的方式是用 clang -e 选项,它仅执行预处理阶段,展开宏、插入头文件、处理条件编译后将结果输出到 stdout 或指定 .i 文件,需显式指定 -x c 或 -x c++ 语言模式。

clang -E 只做预处理,输出到 stdout
想让 LLVM(准确说是 Clang 前端)只跑预处理阶段、不生成 IR、不汇编、不链接,最直接的方式就是用 -E 选项。它会终止在预处理后,把展开宏、插入头文件、执行条件编译后的纯 C/C++ 代码打印到标准输出。
- 默认不写
-o时,结果直接刷屏;加-o out.i可保存为预处理后的.i文件(注意不是.cpp) - 必须显式加
-x c或-x c++,否则 Clang 可能因无扩展名或内容模糊而报错error: unable to handle compilation, expected exactly one compiler job - 若源文件里有
#include <vector></vector>这类系统头,-E仍会展开——但不会检查语法错误,所以即使main()缺少右括号也不会报错 - 和
cpp命令行为基本一致,但 Clang 的预处理器更严格,比如对未定义宏的#if UNDEF默认当 0 处理,而 GCC 可能警告
clang -ccc-print-phases 查看完整阶段,确认预处理是独立环节
LLVM 编译流程被明确划分为输入 → 预处理 → 编译 → 后端 → 汇编 → 链接。用 clang -ccc-print-phases main.cpp 能看到每个阶段的输入/输出类型和依赖关系。你会看到第 1 步是 preprocessor,输出类型为 c++-cpp-output ——这正是 -E 所停的位置。后续所有步骤都依赖这一步的输出,跳过它就无法继续。
- 输出中类似
+-1: preprocessor, {0}, c++-cpp-output的格式,{0} 表示依赖第 0 步(即输入文件),清晰表明预处理是第一个可剥离的完整阶段 - 如果误用
-S(生成汇编)或-c(生成目标文件),流程会走到第 3 或第 4 步,完全违背“只预处理”目标 - 某些 IDE 或构建系统(如 CMake)可能隐式加了
-frecord-gcc-switches等标志,干扰预处理输出;加-v可看到 Clang 实际调用的子命令,确认没额外注入阶段
避免误入“伪预处理”陷阱:-save-temps 和 -Xclang -dump-header
有人试过 clang -save-temps 想留中间文件,结果发现生成了 main.i(预处理)、main.s(汇编)、main.o(目标文件)全套——这显然不符合“只做预处理”需求。还有人搜到 -Xclang -dump-header,但它只输出头文件符号信息,不是预处理结果,且已从现代 Clang 中移除,运行会报错 unknown argument: '-dump-header'。
-
-save-temps是调试用的,本质是让整个流程跑完,只是把各阶段中间产物存下来;它不控制流程终点 -
-Xclang是向 Clang 内部传递未公开参数的后门,-dump-header属于历史遗留,Clang 14+ 已弃用,别白费力气 - 真要对比不同宏定义下的预处理结果,正确做法是多次运行
clang -E -DFOO=1 main.cpp和clang -E -DFOO=0 main.cpp,分别重定向输出
预处理看似简单,但 Clang 对输入语言模式、头搜索路径、宏作用域的判断非常敏感;同一行 #include 在 -E 下能展开,在 -c 下却可能因系统头缺失而失败——这意味着你不能假设预处理成功就等于后续阶段一定成功。











