大多数人用宏只会 #define 替换,但预处理器还有两个操作符几乎没人注意到,用好了能省掉大量重复代码。
#:字符串化
在宏参数前加 #,把参数原样变成字符串字面量:
#define STRINGIFY(x) #x
STRINGIFY(hello) // → "hello"
STRINGIFY(123) // → "123"
STRINGIFY(a + b) // → "a + b"
最实用的场景是调试打日志时自动带上变量名:
#define LOG_VAR(x) printf(#x " = %d\n", (x))
int voltage = 3300;
LOG_VAR(voltage); // 输出:voltage = 3300
不用手写字符串,变量改名时日志自动跟着变。
##:符号拼接
把两个符号拼接成一个新符号:
#define REG(n) reg_##n
REG(status) // → reg_status
REG(ctrl) // → reg_ctrl
常见于批量生成函数名或变量名,避免手写大量相似代码:
#define DEFINE_SETTER(field, type) \
void set_##field(type val) { g_config.field = val; }
DEFINE_SETTER(brightness, uint8_t) // 展开为 void set_brightness(uint8_t val) {...}
DEFINE_SETTER(volume, uint8_t) // 展开为 void set_volume(uint8_t val) {...}
X-Macro:把两者推到极致
有了 # 和 ## 的基础,来看一个真正解决实际问题的技巧。
问题场景:定义一组错误码,同时需要一个函数把错误码转成字符串:
// 常见写法——枚举和字符串表分开维护
typedef enum {
ERR_OK,
ERR_TIMEOUT,
ERR_BUSY,
ERR_NO_MEM,
} error_t;
static const char *err_names[] = {
"ERR_OK",
"ERR_TIMEOUT",
"ERR_BUSY",
"ERR_NO_MEM",
};
每次新增一个错误码,要改两个地方。漏改一个,字符串表就对不上,而且编译器不会报错。
X-Macro 写法:
用一个宏列表作为唯一数据源,枚举和字符串表都从它生成:
// 唯一数据源
#define ERROR_LIST \
X(ERR_OK, "Success") \
X(ERR_TIMEOUT, "Timeout") \
X(ERR_BUSY, "Busy") \
X(ERR_NO_MEM, "Out of memory")
// 生成枚举
typedef enum {
#define X(code, str) code,
ERROR_LIST
#undef X
} error_t;
// 生成字符串表
static const char *err_str[] = {
#define X(code, str) [code] = str,
ERROR_LIST
#undef X
};
// 转换函数
const char *error_to_str(error_t e) {
if (e < sizeof(err_str) / sizeof(err_str[0])) return err_str[e];
return "Unknown";
}
新增错误码只改 ERROR_LIST 一行,枚举、字符串表、顺序全部自动同步,物理上不可能不一致。
还能生成什么
X-Macro 不限于枚举+字符串,同一张表可以同时生成多种产物:
#define CMD_LIST \
X(CMD_START, 0x01, do_start, "start") \
X(CMD_STOP, 0x02, do_stop, "stop") \
X(CMD_RESET, 0x03, do_reset, "reset")
// 生成枚举
typedef enum {
#define X(name, code, fn, str) name = code,
CMD_LIST
#undef X
} cmd_t;
// 生成函数指针表(结合上一篇的技巧)
typedef void (*cmd_fn_t)(void);
static const struct { uint8_t code; cmd_fn_t fn; const char *name; } cmd_table[] = {
#define X(name, code, fn, str) { code, fn, str },
CMD_LIST
#undef X
};
命令码、处理函数、名称字符串,三者永远绑在一起。
注意事项
#undef X每次用完必须加,否则下次重定义X会产生警告或冲突- 宏展开没有类型检查,错误提示可能指向展开后的行,不好定位
- 适合数量多、结构规整、需要多种形式的数据;逻辑复杂的场景不要强行套


没有回复内容