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

字符串化操作符 # 用来把宏参数转成字符串字面量
它只在宏定义内部有效,作用是把传入的**预处理标记(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 在拼接前**各自已完成宏展开**(除非被 # 或另一个 ## 阻止),这点和 # 行为不同。
立即学习“C++免费学习笔记(深入)”;
- 不能出现在宏定义开头或结尾,如
##x或x##非法 - 如果某侧展开为空,需用“空宏”配合
##实现条件拼接(例如可选后缀) - 常用在泛型模拟、事件注册、状态机跳转标签生成中
#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]])替代部分宏用途。
真正难调试的不是语法错误,而是展开后语义突变:比如拼出的函数名在链接时找不到,或字符串化后的名字和实际变量名大小写不一致,导致断点失效或日志误导。











