将宏参数转为带双引号的字符串字面量,不展开;##将两记号拼接成新标识符;二者需配合间接宏处理内置宏或嵌套展开,且须注意空参数、非法标识符及展开顺序。

宏里加#号:把参数变成字符串字面量
预处理器遇到 # 会把紧随其后的宏参数原样转成带双引号的字符串,不展开、不计算。比如写日志或调试时想打印变量名本身,而不是它的值,就得靠它。
常见错误是以为 # 能展开嵌套宏——它不能。如果参数本身是另一个宏,# 只包住那个宏名,不是它展开后的结果。
-
LOG(x)定义为#define LOG(x) printf("value of " #x " is %d\n", x),调用LOG(i)输出value of i is 42 - 但若
VAL是个宏#define VAL 100,LOG(VAL)打印的是value of VAL is 100,不是value of 100 is 100 - 要先展开再字符串化,得套一层间接宏:
#define STR(x) #x和#define XSTR(x) STR(x),然后用XSTR(VAL)得到"100"
宏里用##:拼接两个记号成一个新标识符
## 是“记号粘合器”,把左右两边预处理后的记号(token)强行拼成一个。常用于生成变量名、函数名或类型名,比如按版本或配置生成不同后缀的函数。
最容易踩的坑是拼出来的东西不是合法标识符——比如前后都是空、中间有空格、拼出数字开头的名字,或者拼完后刚好是关键字(如拼出 int_t 但 int 是关键字),预处理器不报错,但编译器会报错。
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
-
CONCAT(a, b)定义为#define CONCAT(a, b) a##b,CONCAT(foo, _v1)展开为foo_v1 - 不能写
CONCAT(int, _t),因为int_t合法,但CONCAT(123, abc)拼出123abc,非法标识符 - 参数为空时可能意外拼出奇怪东西,比如
CONCAT(x, )变成x(空参数被忽略),但CONCAT(, y)会报错:前置空 token 不允许
实际组合用法:带调试信息的断言宏
真实项目里 # 和 ## 经常一起出现,典型例子是自定义 assert:既要打印表达式原文(用 #),又要生成唯一错误处理函数名(用 ##)。
注意行号和文件名这类内置宏(__LINE__、__FILE__)必须通过间接宏才能进 # 或 ##,否则它们会被当字面量处理,而不是展开成数字或字符串。
- 直接写
#__LINE__得到"__LINE__";要得到"123",得用#define STRINGIFY(x) #x+STRINGIFY(__LINE__) - 生成函数名如
assert_fail_123:先用##拼assert_fail_##__LINE__,但需两层宏绕过__LINE__的延迟展开 - 完整套路:
#define ASSERT(x) do { if (!(x)) assert_fail_impl(__FILE__, __LINE__, #x); } while(0)—— 这里#x记录条件,__FILE__和__LINE__传给函数,不参与拼接
宏展开顺序和括号陷阱
# 和 ## 在宏体中优先级最高,但整个宏参数先做一次展开(除非被 # 或 ## 阻止),再执行字符串化或拼接。这个顺序决定了你能不能拿到想要的结果。
最隐蔽的问题是忘记给宏参数加括号——尤其在 ## 左右有运算符时,比如 ADD(x, y) 定义为 #define ADD(x, y) x##+##y,调用 ADD(1, 2) 会试图拼出 1+2,这不是合法标识符,编译失败。而 # 不吃括号, #(x+y) 会变成 "(x+y)",但如果你本意是先算再转字符串,宏做不到。
- 参数含逗号时,必须用括号包裹整个参数,否则预处理器按逗号分隔参数,比如
MACRO((a, b), c)中第一个参数才是(a, b) -
##两端不能是预处理关键字(如##include)、字符串字面量或注释 - 标准不保证
##拼接后是否重新扫描,所以别指望拼出的新记号能自动触发下一轮宏展开
宏里的 # 和 ## 看似简单,但一旦涉及多层展开、空参数、内置宏或非法拼接,错误往往只在编译时报出来,且提示位置指向展开后的代码,不是原始宏定义。动手前先想清楚:这里到底需要字面量,还是真要拼标识符;参数会不会为空;拼出来的东西编译器认不认识。

















