# 和 ## 运算符:字符串化与标记粘贴
各位嵌入式开发者,今天我们来聊聊预处理中两个非常实用、但也容易让人困惑的运算符——# 和 ##。
说实话,我刚入行那会儿,看到这两个符号在宏定义里出现,第一反应是「这什么鬼?」。后来在项目中踩过几次坑,才真正体会到它们的威力。今天我就把这两把「瑞士军刀」的用法掰开揉碎了讲给你听。
一、# 运算符:字符串化操作符
# 的作用很简单:把宏参数变成字符串字面量。说白了,就是给参数加一对双引号。
来看个最基础的例子:
#define STR(x) #x
printf("%s\n", STR(hello)); // 输出: hello
printf("%s\n", STR(123)); // 输出: 123
你可能会问:「这有啥用?我直接写字符串不就行了?」
嗯,别急。在实际项目中,它的价值体现在调试信息输出和错误处理上。我举个例子:
#define ASSERT(cond) \
do { \
if (!(cond)) { \
printf("断言失败: %s, 文件: %s, 行号: %d\n", \
#cond, __FILE__, __LINE__); \
abort(); \
} \
} while(0)
// 使用
int value = 5;
ASSERT(value > 10);
// 输出: 断言失败: value > 10, 文件: test.c, 行号: 42
看到没?#cond 把条件表达式原封不动地变成了字符串。这样你一眼就能看出是哪个条件没通过,不用再去翻代码。
我的习惯:在调试宏中,我通常会把参数名、参数值、表达式一起打印出来。比如:
#define DEBUG_VAR(var) \
printf(#var " = %d [%s:%d]\n", var, __FILE__, __LINE__)
这样调试信息一目了然。
二、## 运算符:标记粘贴操作符
## 的作用是把两个标记「粘」在一起,形成一个新的标记。它不能出现在宏定义的末尾,也不能单独使用——必须左右都有东西。
我最常用的场景是生成变量名和函数名。比如:
#define MAKE_VAR(name, index) name##_##index
int MAKE_VAR(temp, 1) = 10; // 展开为: int temp_1 = 10;
int MAKE_VAR(temp, 2) = 20; // 展开为: int temp_2 = 20;
你想想看,如果有一组结构体需要注册回调函数,用 ## 能省多少重复代码:
#define REGISTER_DRIVER(type) \
static int type##_init(void); \
static int type##_deinit(void); \
static struct driver_ops type##_ops = { \
.init = type##_init, \
.deinit = type##_deinit, \
};
REGISTER_DRIVER(uart) // 生成 uart_init, uart_deinit, uart_ops
REGISTER_DRIVER(spi) // 生成 spi_init, spi_deinit, spi_ops
REGISTER_DRIVER(i2c) // 生成 i2c_init, i2c_deinit, i2c_ops
我在做嵌入式驱动框架时,就是用这种方式统一了所有外设的注册接口。后来团队新来的同事看到这段代码,第一反应是「这代码是自动生成的吗?」——其实就是一个宏搞定的。
三、# 和 ## 的联合使用
这两个运算符可以组合使用,威力更大。来看一个我项目中实际用过的例子:
#define LOG_ERROR(mod, fmt, ...) \
do { \
printf("[ERROR][%s] " fmt "\n", #mod, ##__VA_ARGS__); \
} while(0)
LOG_ERROR(uart, "发送超时, 重试次数: %d", retry);
// 输出: [ERROR][uart] 发送超时, 重试次数: 3
这里 #mod 把模块名变成字符串,##__VA_ARGS__ 是 GNU C 的扩展用法——当可变参数为空时,它会「吃掉」前面的逗号,避免编译错误。
我曾经踩过的坑:
##不能生成注释标记(//或/* */),预处理器在宏展开前已经处理了注释##不能生成预处理指令(如#include、#define),这些指令在宏展开前已经执行- 嵌套宏中,
#和##的展开顺序有讲究——先展开参数,再应用#或##
四、知识体系总览
下面这张图帮你理清这两个运算符的核心逻辑:
五、实战中的注意事项
讲完了原理,我再分享几个实战中容易踩的坑:
- 宏参数展开顺序:如果宏参数本身也是宏,
#和##会先展开参数,再应用运算符。但如果你用#或##阻止了展开,那就不会展开。这个细节容易让人困惑。 - 空格处理:
#在生成字符串时,会保留参数中的空格,但会忽略参数前后的空格。比如STR( hello world )会变成"hello world"。 - 与可变参数配合:GCC 的
##__VA_ARGS__扩展很实用,但不是标准 C。如果你写跨平台代码,建议用__VA_OPT__(C23 标准)。
我的建议:
- 调试宏中多用
#,让日志信息更清晰 - 驱动框架、注册表中多用
##,减少重复代码 - 两个运算符一起用时,先写小例子验证展开结果
- 可以用
gcc -E查看预处理后的代码,这是调试宏的利器
好了,关于 # 和 ## 的内容就讲到这里。这两个运算符虽然看起来简单,但用好了能让你的代码更简洁、更优雅。下次写调试宏或者驱动框架时,不妨试试它们。