预处理器

原理

编译四阶段

C++ 的编译流程分为四个阶段:预处理 → 编译 → 汇编 → 链接。预处理器位于第一阶段,其本质是一个纯文本替换引擎——它不理解 C++ 语法,只是机械地进行宏展开、文件包含和条件编译。

预处理阶段的输出是”翻译单元”(translation unit)——所有 #include 的文件被内联、所有宏被展开、条件编译块被判定、注释被移除。可通过 g++ -E source.cpp -o source.i 观察。

文本替换的本质

预处理器维护一个宏定义表(内部字典),遇到标识符时查找此表。若匹配则替换,替换后的文本若再含宏标识符则递归展开。预处理器不理解如下 C++ 概念:类型、作用域、重载、模板、constexpr

这也是为什么宏参数和宏体都要用括号包裹——预处理器不做运算,生成的代码直接交给编译器按运算符优先级解析:

#define BAD_SQUARE(x) x * x
// BAD_SQUARE(3+1) → 3+1*3+1 = 7 (期望 16)
 
#define SQUARE(x) ((x) * (x))
// SQUARE(3+1) → ((3+1)*(3+1)) = 16
```cpp
 
### Token Pasting(##)
 
`##` 将两个预处理记号连接成新记号。它在宏展开流程中的求值优先级**高于**宏展开——先进行 token 连接,再进行展开。因此需要双层宏来实现"先展开参数再连接"
 
```cpp
#define CONCAT(a, b) a ## b
#define EXPAND_AND_CONCAT(a, b) CONCAT(a, b)
// CONCAT(counter, __LINE__) → counter__LINE__ (字面连接)
// EXPAND_AND_CONCAT(counter, __LINE__) → counter42 (先展开再连接)
```cpp
 
### Stringification(#)
 
`#` 将宏参数转为字符串字面量,同时对特殊字符(`\`、`"`)进行转义:
 
```cpp
#define STR(x) #x
#define DBG(var) std::cout << #var " = " << (var) << std::endl
// DBG(x + y) → std::cout << "x + y" " = " << (x + y) << std::endl
```cpp
 
### 条件编译与链接器去重
 
`#ifdef`/`#ifndef`/`#endif` 在预处理阶段决定哪些代码会被保留。这完全发生在编译之前,被排除的代码不会进入翻译单元。
 
模板函数的实例化会在多个编译单元中产生相同代码。链接器通过 COMDAT 折叠(或 ICF)进行去重,最终只保留一份。这与条件编译形成了互补——前者控制"哪些实例化需要生成",后者控制"生成代码的编译范围"。
 
### 预处理器与 C 语言的关系
 
预处理器的语法源自 C 语言。C 中的 `#define` 用法和陷阱见 [[../../c语言教程/2深化/05_位运算与硬件操作]] 的宏部分。
 
---
 
## 语法
 
### #include
 
```cpp
#include <iostream> // 系统头文件路径
#include "myheader.h" // 先在当前目录搜索,再搜索系统路径
```cpp
 
### #define / #undef
 
```cpp
#define PI 3.14159
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SWAP(a, b) do { auto tmp = (a); (a) = (b); (b) = tmp; } while(0)
#undef MAX // 取消宏定义
```cpp
 
> 使用 `do { ... } while(0)` 包裹多语句宏体可防止悬挂 else 问题。
 
### 可变参数宏(C++11)
 
```cpp
#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
// ##__VA_ARGS__ 是 GCC/Clang 扩展:空参数时自动移除前面的逗号
// C++20 标准替代:__VA_OPT__
```cpp
 
### 条件编译
 
```cpp
#ifdef DEBUG
 #define DLOG(x) std::cout << x << std::endl
#else
 #define DLOG(x) ((void)0)
#endif
 
#if __cplusplus >= 201703L
 // C++17 及以上
#endif
 
#if defined(__GNUC__) && !defined(__clang__)
 // GCC 编译器
#elif defined(__clang__)
 // Clang 编译器
#endif
```cpp
 
### 头文件守卫
 
```cpp
#ifndef MYHEADER_H
#define MYHEADER_H
// 头文件内容
#endif
 
// 或使用(非标准但广泛支持)
#pragma once
```cpp
 
### 预定义宏
 
| 宏 | 含义 |
|---|---|
| `__FILE__` | 当前源文件名(字符串) |
| `__LINE__` | 当前行号(整数) |
| `__DATE__` | 编译日期 "Mmm dd yyyy" |
| `__TIME__` | 编译时间 "hh:mm:ss" |
| `__cplusplus` | C++ 标准版本:199711L(C++98), 201103L(C++11), 201703L(C++17), 202002L(C++20) |
| `__func__` | 当前函数名(C++11,非宏) |
 
### 其他指令
 
```cpp
#error "Unsupported platform" // 编译期报错
#pragma pack(1) // 结构体字节对齐
#pragma message("compiling...") // 编译期输出信息
```cpp
 
---
 
## 实践
 
**练习**:无专属练习题。预处理器是构建系统和跨平台代码的核心——实际练习建议在真实项目中配置 CMake 的 Debug/Release 构建,观察 `#ifdef NDEBUG` 如何控制断言行为。