概述
预处理阶段的本质
- 对源码做文本级别的变换,把编译器不认识的预处理指令(#开头的东西)全部处理掉,输出一个纯粹的、编译器可以直接解析的
C/C++代码文件 - 宏替换是其中最核心的操作,但
#include展开和条件编译同样重要——实际项目中,#include展开带来的代码量远超宏替换
示例
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# 1 "test.cpp" ← 开始处理 test.cpp # 1 "<built-in>" 1 ← 进入编译器内置定义 # 404 "<built-in>" 3 ← 内置定义有 404 行(宏定义等) # 1 "<command line>" 1 ← 进入命令行定义(-D 参数指定的宏) # 1 "<built-in>" 2 ← 从命令行返回 # 1 "test.cpp" 2 ← 回到 test.cpp 第 1 行 # 1 "/usr/.../include/c++/12/cstdio" 1 3 ← #include <cstdio> 展开,进入系统头文件 # 40 "/usr/.../include/c++/12/cstdio" 3 ← cstdio 的第 40 行 ... ← cstdio 又 include 了其他头文件 ... ← 层层嵌套展开 # 329 "/usr/.../bits/c++config.h" 3 ← 在 c++config.h 的第 329 行 ... |
规则
Linemarker 的完整格式
- 行号和文件名是必须的,标志是可选的,可以有零个、一个或多个
- 标志之间用空格分隔
|
1 |
# 行号 "文件名" [标志1] [标志2] ... |
- 四个标志的含义
|
1 2 3 4 5 6 |
标志 含义 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1 进入一个新文件(因为 #include 开始处理一个新文件) 2 从一个文件返回(一个 #include 处理完毕,回到上层文件) 3 后续内容来自系统头文件(/usr/include 等标准路径下的文件) 4 后续内容应被视为包裹在 extern "C" 中 |
示例解析
- 没有标志。这是最开始的声明:
- "我们在
test.cpp的第1行" - 文件处理的起点,不需要标志
- "我们在
|
1 |
# 1 "test.cpp" |
- 标志
1。- 进入了一个新"文件"——编译器的内置定义(不是真实文件,是编译器自动注入的预定义宏,比如
__GNUC__、__x86_64__、__cplusplus等)
- 进入了一个新"文件"——编译器的内置定义(不是真实文件,是编译器自动注入的预定义宏,比如
|
1 |
# 1 "<built-in>" 1 |
- 标志
3。- 告诉编译器把内置定义当作系统头文件对待(抑制某些警告)
|
1 |
# 1 "<built-in>" 3 |
- 标志
3。- 还在内置定义中,跳到了第
404行 - 中间那些行是几百个预定义宏,被预处理器省略了(因为宏定义在预处理后消失,只留下宏展开的结果)
- 还在内置定义中,跳到了第
|
1 |
# 404 "<built-in>" 3 |
- 标志
1。- 进入了"命令行"这个虚拟文件——处理你在编译命令中通过
-D指定的宏定义 - 你没有指定任何
-D,所以这部分是空的
- 进入了"命令行"这个虚拟文件——处理你在编译命令中通过
|
1 |
# 1 "<command line>" 1 |
- 标志
2。- 从命令行虚拟文件返回到上层(
built-in)
- 从命令行虚拟文件返回到上层(
|
1 |
# 1 "<built-in>" 2 |
- 标志
2。- 从内置定义返回,回到
test.cpp第1行。现在开始处理你的源码了
- 从内置定义返回,回到
|
1 |
# 1 "test.cpp" 2 |
- 标志
1和3。- 两个标志同时出现,含义是:进入一个新文件(标志
1)并且这个文件是系统头文件(标志3)。 - 你的代码第
1行是#include <cstdio>,所以预处理器现在进入了cstdio这个系统头文件
- 两个标志同时出现,含义是:进入一个新文件(标志
|
1 |
# 1 "/usr/bin/../lib/gcc/x86_64-linux-gnu/12/../../../../include/c++/12/cstdio" 1 3 |
- 只有标志
3。- 还在
cstdio文件中,跳到了第40行(前面的行是注释和条件编译指令,预处理后被去掉了) - 没有标志
1因为没有进入新文件,没有标志2因为没有返回上层。只保留标志3提醒编译器这仍然是系统头文件
- 还在
|
1 |
# 40 "/usr/bin/../lib/gcc/x86_64-linux-gnu/12/../../../../include/c++/12/cstdio" 3 |
- 标志
1和3。cstdio内部又#include了bits/c++config.h,进入了这个新的系统头文件
|
1 |
# 1 "/usr/bin/../lib/gcc/x86_64-linux-gnu/12/../../../../include/x86_64-linux-gnu/c++/12/bits/c++config.h" 1 3 |
- 只有标志
3- 还在
c++config.h中,跳到第296行
- 还在
|
1 |
# 296 "/usr/bin/../lib/gcc/x86_64-linux-gnu/12/../../../../include/x86_64-linux-gnu/c++/12/bits/c++config.h" 3 |
- 只有标志
3- 同一个文件内跳到第
329行
- 同一个文件内跳到第
|
1 |
# 329 "/usr/bin/../lib/gcc/x86_64-linux-gnu/12/../../../../include/x86_64-linux-gnu/c++/12/bits/c++config.h" 3 |
标志组合的所有可能性
|
1 2 3 4 5 6 7 8 9 10 11 12 |
标志 含义 出现场景 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (无标志) 同一文件内的行号跳转 宏展开或条件编译跳过了一些行 1 进入一个新的用户文件 #include "user_header.h" 2 从一个文件返回上层 某个 #include 处理完毕 3 当前内容是系统头文件 在 /usr/include 等系统路径下 4 后续内容应视为 extern "C" C 语言的系统头文件 1 3 进入一个新的系统头文件 #include <system_header.h> 2 3 从一个系统头文件返回到另一个系统头文件 嵌套的系统头文件展开完毕 1 3 4 进入一个新的 C 语言系统头文件 C++ 中 #include <cstdio> 最终 include 了 C 的 <stdio.h> 2 3 4 从 C 语言系统头文件返回 stdio.h 处理完毕返回 cstdio |
实际的 include 嵌套
|
1 2 3 4 5 6 7 8 9 10 |
test.cpp ← 你的文件 └─ #include <cstdio> ← # ... "cstdio" 1 3(进入,系统头文件) └─ #include <bits/c++config.h> ← # ... "c++config.h" 1 3 │ └─ (各种基础类型定义) │ └─ 返回 cstdio ← # ... "cstdio" 2 3(返回,系统头文件) └─ #include <stdio.h> ← # ... "stdio.h" 1 3 4(进入,C系统头文件) │ └─ (printf 等函数声明) │ └─ 返回 cstdio ← # ... "cstdio" 2 3(返回) └─ cstdio 处理完毕 返回 test.cpp ← # ... "test.cpp" 2(返回) |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 动手验证 # 只看 linemarker 行,观察 include 嵌套关系 grep "^#" test.i | head -30 # 统计总共涉及了多少个头文件 grep "^#" test.i | grep -oP '"[^"]+"' | sort -u # 你会看到 cstdio 一个 #include 拉进了十几个头文件 # 只看标志为 1 的行(进入新文件),理解 include 链 grep "^#.*\" 1" test.i # 只看标志为 2 的行(返回上层),理解嵌套深度 grep "^#.*\" 2" test.i # 看 extern "C" 相关的标志 4 grep "^#.*4$" test.i # 这些就是 C 头文件(如 stdio.h)被引入的位置 |
预处理器
预处理器的完整工作清单
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
预处理器做的所有事情(按处理顺序): 1. 三字符组替换(Trigraph) ??= → # ??( → [ ??/ → \ 现代代码基本不会遇到,C++17 已经移除了这个特性 2. 行拼接(Line Splicing) 把以 \ 结尾的行和下一行合并成一行 #define LONG_MACRO(a, b, c) \ do_something(a); \ do_other(b); \ finish(c) → 合并成一个逻辑行 3. 注释去除(Comment Removal) // 单行注释 → 替换为一个空格 /* 多行注释 */ → 替换为一个空格 编译器看不到任何注释 4. ★ #include 文件包含 把目标文件的完整内容插入到当前位置 递归处理(被包含的文件中的 #include 也会展开) 这就是你的 10 行代码变成 800 行的原因 5. ★ #define 宏定义与替换 对象宏:#define PI 3.14159 → 代码中所有 PI 替换为 3.14159 函数宏:#define MAX(a,b) ((a)>(b)?(a):(b)) → 调用处展开 6. ★ 条件编译 #if / #ifdef / #ifndef / #elif / #else / #endif 根据条件决定哪些代码保留、哪些代码丢弃 #ifdef __linux__ // Linux 专用代码 → 保留 #elif defined(_WIN32) // Windows 专用代码 → 整段丢弃 #endif 7. 特殊指令处理 #pragma → 编译器特定指令,原样传递给编译器 #error → 产生编译错误(常用于检测配置) #warning → 产生编译警告 #line → 修改当前行号和文件名(代码生成器常用) 8. 预定义宏展开 __FILE__ → 当前文件名字符串 __LINE__ → 当前行号 __DATE__ → 编译日期 __TIME__ → 编译时间 __func__ → 当前函数名(严格说这个是编译器处理的,不是预处理器) __cplusplus → C++ 标准版本号 9. 输出行标记(Linemarker) 就是你在 test.i 中看到的那些 # 行号 "文件名" 标志 告诉编译器后续代码的原始位置 |
预处理后什么消失了,什么保留了
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
消失了(编译器完全看不到): ━━━━━━━━━━━━━━━━━━━━━━━━━━ - 所有 #define 指令本身(只留下替换后的结果) - 所有 #include 指令(只留下被包含的文件内容) - 所有 #ifdef / #endif 条件编译指令 - 所有注释(// 和 /* */) - 被条件编译排除的代码段 保留了(编译器会看到): ━━━━━━━━━━━━━━━━━━━━━━━━━━ - 宏替换后的结果(代码中不再有任何宏名) - #include 展开后的完整内容 - 条件编译选中的分支的代码 - #pragma 指令(原样传递给编译器) - 行标记(# 行号 "文件名" 标志) - 所有的类型定义、函数声明、变量定义等实际代码 |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Linux下修改用户密码记录08/08
- ♥ Linux 内存映射与普通文件访问的区别03/31
- ♥ Linux_ 命令大全 Windows System03/16
- ♥ Linux_命令大全 压缩备份03/16
- ♥ Linux 高性能服务器编程:服务器程序规范12/04
- ♥ 关于SSH08/18