缺乏类型检查: 预处理器不做类型检查。如果宏参数类型不匹配或使用不当,错误可能要到编译甚至运行时才暴露,且错误信息可能指向宏展开后的代码,难以调试。
多次求值: 如果宏参数在替换文本中出现多次,而实参是一个有副作用(如自增 i++、函数调用)的表达式,那么这个表达式会被求值多次,导致意料之外的结果。
#define DOUBLE(x) ((x) + (x))
int i = 5;
int j = DOUBLE(i++); // 替换后: int j = ((i++) + (i++)); // i 被加了两次!结果未定义(取决于编译器求值顺序)
作用域问题: 宏没有作用域概念。#define 从定义点开始到文件末尾(或遇到 #undef)都有效。可能无意中覆盖其他地方定义的宏或变量名(通常用全大写加下划线命名宏来减少冲突)。
调试困难: 调试器看到的是宏展开后的代码,而不是原始的宏定义和调用,这使得跟踪问题变得更复杂。
运算符优先级陷阱: 如前所述,函数式宏中的参数和整个表达式必须用括号仔细包裹,否则极易因运算符优先级导致逻辑错误(如 BAD_SQUARE 的例子)。
可能产生庞大代码: 如果一个宏非常大且被多次调用,它会在每个调用点都展开一次,可能导致生成的目标代码体积显著增大(称为代码膨胀)。函数代码在内存中通常只有一份。
宏 vs. 函数 vs. 常量变量
特性 | 宏 (函数式) | 函数 | 常量变量 (const) / 枚举 (enum) | 处理阶段 | 预处理 (文本替换) | 编译 | 编译 | 类型检查 | 无 | 有 | 有 (定义时确定类型) | 参数求值次数 | 参数出现几次就求值几次 | 参数在调用前仅求值一次 | 不适用 | 副作用风险 | 高 (参数多次求值) | 低 | 无 | 代码生成 | 每次调用都展开 (可能膨胀) | 通常一份代码 (调用开销) | 直接使用值 | 调试 | 困难 (看到展开后代码) | 容易 | 容易 | 作用域 | 文件作用域 (或 #undef) | 函数作用域 / 块作用域 | 块作用域 / 文件作用域 | 开销 | 无运行时调用开销 | 有函数调用开销 | 无开销 | 主要用途 | 文本替换、条件编译、小操作内联、泛型模拟 | 封装功能、结构化代码、复用逻辑 | 定义不可变的常量值 | 安全性 | 低 (易出错) | 高 | 高 |
|