基础知识
预处理展开主要指 C/C++ 中宏(Macro)替换的机制,核心遵循“由内向外、参数预扫描、防递归”原则。
预处理核心展开流程
- 参数预扫描(Prescan):在将实参替换入宏体前,先对实参进行完全展开。若实参本身是宏,则先展开为最终值,再代入。
- 宏体替换与再扫描:将展开后的实参替换进宏定义主体,然后对结果重新扫描,继续展开其中包含的其他宏,直到无可展开宏为止。
- 禁止直接递归:若宏在展开过程中再次遇到自身名称,预处理器会标记该宏为“正在展开”,停止进一步展开以防止无限循环。
特殊运算符的例外规则
当宏参数涉及以下运算符时,参数预扫描被抑制,参数保持原貌:
#(字符串化):参数不被展开,直接转换为字符串字面量。例如 #x 会将 x 转为 "x",即使 x 是宏也不展开。##(令牌粘贴):参数不被展开,直接与相邻令牌合并为新标识符。合并后的结果若构成新宏,会在后续扫描中展开。
注意:若参数未受 # 或 ## 保护,即使包含逗号也不视为参数分隔符,而是作为整体参与展开。
关键执行顺序
- 文件包含优先:
#include 在宏展开前处理,确保被包含文件中的宏定义可用。 - 条件编译依赖宏值:
#if 等指令评估前需完成相关宏的展开,以获取正确表达式值。 - 单次扫描逻辑:预处理大致为单次扫描过程,宏展开产生的文本不会重新解析为新的预处理指令(如 #define)。
常见陷阱
- 优先级错误:函数式宏体及参数建议加括号,如
#define SQUARE(x) ((x)*(x)),避免 SQUARE(1+2) 展开为 1+2*1+2=5 而非 9。 - 间接展开技巧:若需对
# 或 ## 的参数进行展开,需通过两层宏定义(如 STRINGIFY(x) 调用 STRINGIFY_HELPER(x))利用两步展开机制实现。
可通过编译器选项(如 GCC 的 -E)查看预处理后的完整代码以验证展开结果。
X Macro
先定义一个包含多条数据的统一列表,然后通过临时改变 X 的含义,在不同上下文里重复使用这个列表,自动生成不同的代码,比如枚举、字符串数组、switch 分支等。
其核心思想是通过 #define 和 undef 控制宏的作用范围,使得 X 宏在不同的地方有不同的表达。
优缺点
优点
- 单一数据源:所有关联信息(如名称、缩写、数值)集中在一处,修改时只需改一个地方。
- 避免重复:不会因为多拷贝同一组数据而出现不一致。
- 维护性强:新增或删除一项数据后,所有自动生成的代码都会同步更新,减少遗漏。
缺点
- 可读性下降:对不熟悉 X Macro 的人来说,代码比较晦涩。
- 调试困难:预处理后的代码难以直接查看,编译错误信息可能指向展开后的位置,不直观。
- IDE 支持差:代码跳转、自动补全等通常对宏展开的支持有限。
示例:实现枚举转字符串
1 2 3 4 5 6 7 8
| #define LOG_LEVELS \ X(Trace, T) \ X(Debug, D) \ X(Info, I) \ X(Warning, W) \ X(Error, E) \ X(Fatal, F)
|
1 2 3 4 5 6
| enum class LogLevel : uint8_t { #define X(name, short) name, LOG_LEVELS #undef X };
|
展开为
1 2 3 4 5 6 7 8 9
| enum class LogLevel : uint8_t { Trace, Debug, Info, Warning, Error, Fatal };
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| #define STRINGIFY(x) #x constexpr const char *LogLevelNames[] = { #define X(name, short) STRINGIFY(name), LOG_LEVELS #undef X };
constexpr const char *LogLevelShortNames[] = { #define X(name, short) STRINGIFY(short), LOG_LEVELS #undef X }; #undef STRINGIFY
|
这里定义 STRINGIFY 宏,是为了 先展开实参,再字符串化
如果直接写成
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
| constexpr const char *LogLevelNames[] = { #define X(name, short) #name, LOG_LEVELS #undef X };
constexpr const char *LogLevelNames[] = { #define X(name, short) #name, X(Trace, T) \ X(Debug, D) \ X(Info, I) \ X(Warning, W) \ X(Error, E) \ X(Fatal, F) #undef X };
constexpr const char *LogLevelNames[] = { "Trace", "Debug", "Info", "Warning", "Error", "Fatal",}
|
⚠ 但是如果后期拓展
1 2 3 4 5 6 7 8 9 10
| #define MIN_LOG_LEVEL Trace #define MAX_LOG_LEVEL Fatal
#define LOG_LEVELS \ X(MIN_LOG_LEVEL, T) X(Debug, D) \ X(Info, I) \ X(Warning, W) \ X(Error, E) \ X(MAX_LOG_LEVEL, F)
|
那么 ① 将被转换为 ③
1 2 3 4 5 6 7 8 9 10
| constexpr const char *LogLevelNames[] = { #define X(name, short) #name, X(MIN_LOG_LEVEL, T) \ X(Debug, D) \ X(Info, I) \ X(Warning, W) \ X(Error, E) \ X(MAX_LOG_LEVEL, F) #undef X };
|
再展开为 ④
1 2 3 4 5 6 7
| constexpr const char *LogLevelNames[] = { "MIN_LOG_LEVEL", "Debug", "Info", "Warning", "Error", "MAX_LOG_LEVEL",}
|
这样就不符合预期
使用 STRINGIFY 宏可以解决这个问题
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
| #define STRINGIFY(x) #x constexpr const char *LogLevelNames[] = { #define X(name, short) STRINGIFY(name), LOG_LEVELS #undef X }; #undef STRINGIFY
#define STRINGIFY(x) #x constexpr const char *LogLevelNames[] = { #define X(name, short) STRINGIFY(name), X(MIN_LOG_LEVEL, T) \ X(Debug, D) \ X(Info, I) \ X(Warning, W) \ X(Error, E) \ X(MAX_LOG_LEVEL, F) #undef X }; #undef STRINGIFY
#define STRINGIFY(x) #x constexpr const char *LogLevelNames[] = { STRINGIFY(MIN_LOG_LEVEL) \ STRINGIFY(Debug) \ STRINGIFY(Info) \ STRINGIFY(Warning) \ STRINGIFY(Error) \ STRINGIFY(MAX_LOG_LEVEL) }; #undef STRINGIFY
#define STRINGIFY(x) #x constexpr const char *LogLevelNames[] = { STRINGIFY(Trace) \ STRINGIFY(Debug) \ STRINGIFY(Info) \ STRINGIFY(Warning) \ STRINGIFY(Error) \ STRINGIFY(Fatal) }; #undef STRINGIFY
constexpr const char *LogLevelNames[] = { "Trace", "Debug", "Info", "Warning", "Error", "Fatal",}
|
实现将枚举值转为字符串的接口
1 2 3 4 5 6 7 8 9
| inline const char *LeveltoString(LogLevel level) { return LogLevelNames[static_cast<uint8_t>(level)]; }
inline const char *LeveltoShortString(LogLevel level) { return LogLevelShortNames[static_cast<uint8_t>(level)]; }
|