c++中预处理器(preprocessor)的字符串化(#)和标记粘贴(##)有什么用? (宏技巧)

将宏参数原样转为字符串字面量,不展开、不求值;## 拼接两个标记为新标识符,拼接前各自先展开。二者常组合用于调试宏,需注意展开顺序与空参数处理。

字符串化操作符 # 用来把宏参数转成字符串字面量

它只在宏定义内部有效,作用是把传入的**预处理标记(token)原样包裹成双引号字符串**,不是运行时转换,也不做类型检查或求值。

常见错误是以为 # 能处理变量名以外的东西,比如表达式或带空格的片段——它会直接报错或拼出非法字符串。

  • # 后必须紧跟宏参数名,不能加空格,如 #x 合法,# x 非法
  • 参数若本身是另一个宏,在字符串化前**不会展开**,除非先用中间宏“触发一次展开”
  • 适合日志、调试输出、生成结构体字段名字符串等场景
#define STR(x) #x
#define X 123
STR(X)     // 展开为 "X",不是 "123"
STR(abc)   // 展开为 "abc"
STR(int a) // 展开为 "int a" —— 注意空格也被保留

标记粘贴操作符 ## 用于拼接两个预处理标记

它把左右两边的 token 合并成一个新的合法标识符(identifier),常用于生成变量名、函数名或枚举值。失败时预处理器直接报错,比如拼出非法符号(## 两端不能是纯数字或含运算符)。

容易忽略的是:## 两侧 token 在拼接前**各自已完成宏展开**(除非被 # 或另一个 ## 阻止),这点和 # 行为不同。

  • 不能出现在宏定义开头或结尾,如 ##xx## 非法
  • 如果某侧展开为空,需用“空宏”配合 ## 实现条件拼接(例如可选后缀)
  • 常用在泛型模拟、事件注册、状态机跳转标签生成中
#define CONCAT(a, b) a##b
#define PREFIX ver
CONCAT(PREFIX, 2)   // 展开为 ver2
CONCAT(ver, 2)      // 同样是 ver2
#define EMPTY
CONCAT(foo, EMPTY)  // 展开为 foo(EMPTY 展开为空,## 消除空隙)

组合使用 ### 实现调试宏

真实项目里它们常一起出现,比如打印变量名和值:# 把参数名变字符串,## 拼出临时变量避免命名冲突,或区分不同作用域的同名变量。

最典型的坑是“展开顺序”:想让参

数先展开再字符串化,得套一层宏;否则直接 #x 拿到的是原始形参名。

  • 写两层宏是惯用解法:STR(x)STR_IMPL(x)#x
  • ## 在宏参数为空时可能产生孤立的 ##,GCC/Clang 支持 ##__VA_ARGS__ 消除逗号,但标准 C++ 中需谨慎
  • 不要试图用它们生成字符串字面量再传给 printf 的格式串——那是运行时行为,预处理器不参与
#define STR_IMPL(x) #x
#define STR(x) STR_IMPL(x)
#define VAR_NAME(x) x##_t
STR(STR)           // "STR",不是 "STR_IMPL"
VAR_NAME(int)      // int_t

预处理器宏不是万能的,### 尤其受限

它们只在翻译阶段第 4 步(宏替换)起作用,无法访问类型信息、作用域或模板实例化结果。现代 C++ 更推荐 constexpr、模板别名或属性(如 [[maybe_unused]])替代部分宏用途。

真正难调试的不是语法错误,而是展开后语义突变:比如拼出的函数名在链接时找不到,或字符串化后的名字和实际变量名大小写不一致,导致断点失效或日志误导。