C++ Preprocessing
预处理要做什么
预处理是最早的阶段,将处理我们写的 .cpp/.h 源码 + 宏 + #include 指令等“指令层面”的内容,生成一个“干净的、中间的源码”给后面的编译器看。
预处理阶段具体的工作包括:
- trigraph 替换 / 字符映射(历史遗留,用得比较少)
 - 行拼接 / 逻辑行重组(处理反斜杠换行等)
 - 删除注释(// …、/* … */)
 - 执行 #include:把其他头文件、库头等的内容“插入”到这里。
 - 宏定义 / 展开(
#define/#undef) - 条件编译 / 分支剔除(
#if、#ifdef、#ifndef、#elif、#else、#endif) - 特殊指令处理:#pragma、#line、#error 等
 - 加入辅助标记,比如行号 / 文件名映射,以便编译器后面报错时知道原始位置
 
处理完后,就产生一个“翻译单元”(translation unit),也就是一个包含了所有 #include 展开、宏处理、条件剔除后的源码。这个输出通常以 .i / .ii 为后缀。
根据标准,“Translation Unit”就是预处理后最终交给编译器处理的单位。 
预处理器完全不理解 C++ 的语法、类型等。它只在词法/标记(token)级别工作。也就是说,预处理阶段不做语法检查、类型检查。
测试预处理的产物
假设我们有一个main.cpp文件
1  | 
  | 
我们可以用 GCC / Clang 看预处理后的输出:
1  | g++ -E main.cpp -o main.i  | 
这个产物文件main.i总计大概 66k 行。
- -E 选项告诉编译器“只做预处理,不进入真正的编译阶段”。
 main.i会包含所有#include <iostream>展开后的内容(大量代码),所有宏都被替换,LOG(…) 按条件展开(若 DEBUG 未定义,则 LOG 相关代码被剔除)。
在main.i中你会看到:
- 不存在代码注释
 SQR(a + 1)被替换成((a + 1)*(a + 1))#include <iostream>的具体文件内容也直接写进去了。
预处理后的一些关键结构
行控制指令 (line markers)
1  | # 1 "main.cpp" 2  | 
这些不是我们写的代码,而是预处理器自动生成的,用来告诉编译器“接下来的代码来自哪个文件、哪一行、是不是系统头文件”。
作用是帮助编译器进行文件的定位和导航。方便错误提示时还能对回到原始源文件定位。
宏展开
所有 #define 宏都会被替换成最终的文本。比如:
1  | 
  | 
在预处理后,就会变成
1  | int arr[10];  | 
头文件展开
所有 #include <...> 会被直接展开成对应头文件的内容。
所以 #include <iostream> 最后会在预处理文件里变成几千行标准库实现。
这就是为什么原始main.cpp几行代码,在main.i中会变成了几万行。
条件编译结果
所有 #if、#ifdef、#ifndef、#else、#endif 在预处理后会被“筛选”掉,只保留成立的那部分代码。所以预处理文件里不会再有 #if 之类的逻辑,只保留符合条件判断的代码。
Pragma / 内建定义
有些头文件里会保留 #pragma 指令(比如 #pragma clang diagnostic ignored …),它们不会被展开掉,会原封不动留在结果里。
用户代码
等所有宏、头文件、条件编译处理完之后,你原来写的 main 函数代码会混在最后,和标准库内容一起成为一个完整的、所有的内容全部展开后的文件。