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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#define SQR(x) ((x)*(x))
#ifdef DEBUG
#define LOG(msg) std::cout << msg << std::endl
#else
#define LOG(msg)
#endif

int main() {
int a = 5;
LOG("start");
int b = SQR(a + 1);
std::cout << b << std::endl;
return 0;
}

我们可以用 GCC / Clang 看预处理后的输出:

1
g++ -E main.cpp -o main.i

这个产物文件main.i总计大概 66k 行。

  • -E 选项告诉编译器“只做预处理,不进入真正的编译阶段”。
  • main.i会包含所有 #include <iostream> 展开后的内容(大量代码),所有宏都被替换,LOG(…) 按条件展开(若 DEBUG 未定义,则 LOG 相关代码被剔除)。

main.i中你会看到:

  1. 不存在代码注释
  2. SQR(a + 1) 被替换成 ((a + 1)*(a + 1))
  3. #include <iostream> 的具体文件内容也直接写进去了。

预处理后的一些关键结构

行控制指令 (line markers)

1
2
# 1 "main.cpp" 2
# 514 "<built-in>" 3

这些不是我们写的代码,而是预处理器自动生成的,用来告诉编译器“接下来的代码来自哪个文件、哪一行、是不是系统头文件”。
作用是帮助编译器进行文件的定位和导航。方便错误提示时还能对回到原始源文件定位。

宏展开

所有 #define 宏都会被替换成最终的文本。比如:

1
2
#define SIZE 10
int arr[SIZE];

在预处理后,就会变成

1
int arr[10];

头文件展开

所有 #include <...> 会被直接展开成对应头文件的内容。
所以 #include <iostream> 最后会在预处理文件里变成几千行标准库实现。
这就是为什么原始main.cpp几行代码,在main.i中会变成了几万行。

条件编译结果

所有 #if#ifdef#ifndef#else#endif 在预处理后会被“筛选”掉,只保留成立的那部分代码。所以预处理文件里不会再有 #if 之类的逻辑,只保留符合条件判断的代码。

Pragma / 内建定义

有些头文件里会保留 #pragma 指令(比如 #pragma clang diagnostic ignored …),它们不会被展开掉,会原封不动留在结果里。

用户代码

等所有宏、头文件、条件编译处理完之后,你原来写的 main 函数代码会混在最后,和标准库内容一起成为一个完整的、所有的内容全部展开后的文件。


本网站由 Nooobad 使用 Stellar 1.33.1 主题创建。
除非另有说明,本博客中的所有文章均采用 CC BY-NC-SA 4.0 许可协议。转载时请注明文章来源。