Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
C语言
嵌入式必须要懂 # ## __VA_ARGS__ 的用法
发布于 2020-01-08 09:17:17 浏览:1796
订阅该版
[tocm] `‘#’` 和 `‘##’` 属于预处理标记。 `‘#’` 和 `‘##’` 用于类似函数的宏定义中(或者简称为宏定义函数)。 `‘__VA_ARGS__’` 是 C99 引入的用于支持宏定义函数中使用可变参数。 ## 操作符 ‘#’ 在宏定义展开的时候,标记 `‘#’` 用于将 `‘#’` 后面的宏定义函数中的参数转化为对应的字符串。宏定义函数的参数与预处理标记 `‘#’` 之间出现的每一个空格都会被删除,并删除第一个预处理标记之前和最后一个预处理标记之后的空白字符,但是宏定义函数参数中的空格会保留。 其中,空参数转化为为空,即宏定义函数入参为空,那么展开的时候也为空。 上面的这段话比较难理解,这里为了准确地传达其意义,我们来看一个示例程序。 > 在看到代码后,可以先猜猜可能的输出结果,如果你答对了,那就是真的会了! > 注意,这里我基于 RT-Thread QEMU BSP 进行代码展示,代码真实编译通过,运行正常。 ### 示例程序 A 请看以下代码: ```c #include
#include
#define mkstr(var) (#var) int main(void) { rt_kprintf("hello rt-thread
"); rt_kprintf(mkstr(hello rt-thread)); return 0; } ``` ### 请问: - 它能编译通过吗? - 它能输出什么内容? ### 答案: - 它可以正常编译通过 - 它输出的内容 ``` hello rt-thread hello rt-threadmsh /> ``` 从上面输出的信息可以看到,`hello rt-thread` 字符串被准确地输出到了控制台,但是没有增加回车换行。其中 `msh />` 字符串是 RT-Thread 控制台回显。 如上,代码 `rt_kprintf(mkstr(hello rt-thread));` 中的 `hello rt-thread` 在没有加引号的情况下,被转化成了字符串。 ### 示例程序 B 为示例程序 A 打印的字符串增加回车换行。 ```c #include
#include
#define mkstr(var) (#var) int main(void) { rt_kprintf("hello rt-thread
"); rt_kprintf(mkstr(hello rt-thread
)); return 0; } ``` 有了示例 A 的基础,示例 B 那就是 soeasy,直接在原有的基础上增加 `
` 转义字符即可输出回车换行。 输出结果如下: ``` hello rt-thread hello rt-thread msh /> ``` ### 示例程序 C 我们在示例程序 B 中成功增加了回车换行的输出,但是你有没有想过一个问题,如果你又很多地方用到 `mkstr` 宏定义函数输出信息,那你是不是每一个地方都要增加 `
`,这岂不是很累,有没有好的方法? 好方法当然有,下面介绍下程序中常用的方式,利用 C 语言相邻字符串自动拼接的特性(当然,这是编译器支持的)。代码如下: ```c #include
#include
#define mkstr(var) (#var"
") int main(void) { rt_kprintf("hello rt-thread
"); rt_kprintf(mkstr(hello rt-thread)); return 0; } ``` 以上代码在 `#var` 后面增加了一个字符串 `
`,我们来看宏定义展开过程: ``` -> rt_kprintf(mkstr(hello rt-thread)); -> rt_kprintf("hello rt-thread""
"); -> rt_kprintf("hello rt-thread
"); ``` ### 示例程序 D 预处理标记 `#` 的基本用法已经展示完了,但怎么理解 “宏定义函数的参数与预处理标记 ‘#’ 之间出现的每一个空格都会被删除,并删除第一个预处理标记之前和最后一个预处理标记之后的空白字符”? 请看下面的代码: ```c #include
#include
#define mkstr(var) ("aa" # var "bb") int main(void) { rt_kprintf("hello rt-thread
"); rt_kprintf(mkstr(hello rt-thread)); return 0; } ``` 宏定义 `mkstr(var) ("aa" # var "bb
")` 中的 `# var` 中间有两个空格,根据定义,`#` 号与宏定义函数参数 `var` 中间的两个空格会被删除,但是 `var` 参数中的 `hello rt-thread` 中的空格不会被删除。 继续,根据定义,宏定义 `mkstr(var) ("aa" # var "bb
")` 中只有一个预处理标记 `#`,其作为第一个和最后一个预处理标记,它前面和后面的空格都会被删除。 因此,以上代码预计输出结果为: ``` hello rt-thread aahello rt-threadbb msh /> ``` ### 示例程序 E 在实际操作时,操作符 `#` 被常用于枚举转字符串。以下代码截取自我的 [FlexibleButton 按键库](https://github.com/murphyzhao/FlexibleButton/blob/master/flexible_button_demo.c) 的示例程序。 ```c #define ENUM_TO_STR(e) (#e) typedef enum { USER_BUTTON_0 = 0, USER_BUTTON_1, USER_BUTTON_2, USER_BUTTON_3, USER_BUTTON_MAX } user_button_t; static char *enum_btn_id_string[] = { ENUM_TO_STR(USER_BUTTON_0), ENUM_TO_STR(USER_BUTTON_1), ENUM_TO_STR(USER_BUTTON_2), ENUM_TO_STR(USER_BUTTON_3), ENUM_TO_STR(USER_BUTTON_MAX), }; ``` ## 操作符 ‘##’ ‘##’ 是预处理拼接标记。在宏定义展开的时候,将 ‘##’ 左边的内容,与 ‘##’ 右边的内容拼接到一起。 注意,对于任何一种形式的宏定义,‘##’ 预处理标记都不应出现在*替换列表*的开头或结尾。 关于*替换列表*: 例如宏定义 `‘#define aa(x, y) (x##y)’` 后面的部分 `‘x##y’` 就是替换列表。 预处理拼接符 `##` 常用于使用宏定义批量生成函数或者变量。 ### 示例程序 F ```c #include
#include
#define my_math(x, y) (x##e##y) int main(void) { rt_kprintf("hello rt-thread
"); printf("%e
", my_math(3, 4)); return 0; } ``` 以上代码是科学计数法的格式输出到控制台,输出内容如下: ``` hello rt-thread 3.000000e+04 msh /> ``` ### 示例程序 G 用预处理拼接符 `##` 批量生成函数或者变量。 以下代码截取自 [RT-Thread `finsh_api.h`](https://github.com/RT-Thread/rt-thread/blob/master/components/finsh/finsh_api.h),该段代码用于导出 Finsh 命令,代码如下所示: ```c #define FINSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \ const char __fsym_##cmd##_name[] SECTION(".rodata.name") = #cmd; \ const char __fsym_##cmd##_desc[] SECTION(".rodata.name") = #desc; \ RT_USED const struct finsh_syscall __fsym_##cmd SECTION("FSymTab")= \ { \ __fsym_##cmd##_name, \ __fsym_##cmd##_desc, \ (syscall_func)&name \ }; ``` 该宏定义的应用,如 `list_timer` 命令,如下所示: ```c FINSH_FUNCTION_EXPORT_CMD(list_timer, list_timer, list timer in system); ``` ## 标识符 `__VA_ARGS__` `__VA_ARGS__` 是在 C99 中增加的新特性。虽然 C89 引入了一种标准机制,允许定义具有可变数量参数的函数,但是 C89 中不允许这种定义可变数量参数的方式出现在宏定义中。C99 中加入了 `__VA_ARGS__` 关键字,用于支持在宏定义中定义可变数量参数,用于接收 `...` 传递的多个参数。[
1
](#refer-anchor-1) `__VA_ARGS__` 只能出现在使用了省略号的像函数一样的宏定义里。例如 `#define myprintf(...) fprintf(stderr, __VA_ARGS__)`。 ### 解析不定参 通过宏定义,将多个参数传递给函数,那么函数是如何解析不定参的呢? 这就需要使用标准库头文件 `
` 中的三个宏,分别是 “va_start()”、“va_arg()”、“va_end()”,以及一个可变参类型 “va_list”,示例使用方式借用 RT-Thread 中的 `rt_sprintf` 的实现,代码如下所示: ```c rt_int32_t rt_sprintf(char *buf, const char *format, ...) { rt_int32_t n; va_list arg_ptr; va_start(arg_ptr, format); n = rt_vsprintf(buf, format, arg_ptr); va_end(arg_ptr); return n; } ``` - 首先使用 “va_list” 类型定义一个变量 “arg_ptr” - 然后调用 “va_start(arg_ptr, format);” 函数,第一个入参是 “va_list” 类型,第二个参数是 “rt_sprintf” 函数参数列表中的最后一个定参 “format” - 然后,通过调用 “rt_vsprintf” 函数,根据 “format” 来解析不定参,并将结果存放到 “buf” 中 - 最后,使用 “va_end(arg_ptr);” 来释放不定参列表占用的资源 ## 带 ‘#’ 的标识符 `#__VA_ARGS__` 预处理标记 ‘#’ 用于将宏定义参数转化为字符串,因此 `#__VA_ARGS__` 会被展开为参数列表对应的字符串。 示例: ```c #define showlist(...) put(#__VA_ARGS__) ``` 测试如下: ```c showlist(The first, second, and third items.); showlist(arg1, arg2, arg3); ``` 输出结果分别为: ``` The first, second, and third items. arg1, arg2, arg3 ``` ## 带 ‘##’ 的标识符 `##__VA_ARGS__` `##__VA_ARGS__` 是 GNU 特性,不是 C99 标准的一部分,C 标准不建议这样使用,但目前已经被大部分编译器支持。 标识符 `##__VA_ARGS__` 的意义来自 ‘##’,主要为了解决一下应用场景: ```c #define myprintf_a(fmt, ...) printf(fmt, __VA_ARGS__) #define myprintf_b(fmt, ...) printf(fmt, ##__VA_ARGS__) ``` 应用: ```c myprintf_a("hello"); myprintf_b("hello"); myprintf_a("hello: %s", "world"); myprintf_b("hello: %s", "world"); ``` 这个时候,编译器会报错,如下所示: ``` applications\main.c: In function 'main': applications\main.c:26:57: error: expected expression before ')' token #define myprintf_a(fmt, ...) printf(fmt, __VA_ARGS__) ^ applications\main.c:36:5: note: in expansion of macro 'myprintf_a' myprintf_a("hello"); ``` 为什么呢? 我们展开 `myprintf_a("hello");` 之后为 `printf("hello",)`。因为没有不定参,所以,`__VA_ARGS__` 展开为空白字符,这个时候,printf 函数中就多了一个 ‘,’(逗号),导致编译报错。而 `##__VA_ARGS__` 在展开的时候,因为 ‘##’ 找不到连接对象,会将 ‘##’ 之前的空白字符和 ‘,’(逗号)删除,这个时候 printf 函数就没有了多余的 ‘,’(逗号)。 ## 参考 - [1] [open-std 网站](http://www.open-std.org/jtc1/sc22/wg14/www/standards.html#9899) - [C99 标准 V5.10](http://www.open-std.org/jtc1/sc22/wg14/www/docs/C99RationaleV5.10.pdf) - [C99 公共版本 N1256](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf) - [C11 公共版本 N1570](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf) - [阅读原文](https://murphy.tech/posts/40137057.html) 看到扩展 markdown 支持,但是引用头文件依旧有问题,还是直接纯文本展示吧,不折腾了。
查看更多
6
个回答
默认排序
按发布时间排序
bernard
2020-01-08
这家伙很懒,什么也没写!
记得最坑的是VC++ 6.0不支持这个,然后遇到老的编译器就抓虾,好在后来VS新的版本对标准支持的友好度提升了不少
bernard
2020-01-08
这家伙很懒,什么也没写!
[i=s] 本帖最后由 bernard 于 2020-1-8 12:26 编辑 [/i] 有些编译器是挺坑的,或者编译器的世界其实都不同,都有很多自己的扩展 * VC++ 6.0,不支持很多东西; * VxWorks的编译器(早期吧,现在不清楚了),不支持C代码中使用"//"注释 * Keil MDK的编译器,如果打开' --gnu'参数,基本可以当GCC来使,这个很管用(不过它也会自动定义GCC的宏,伪装成GCC -_-) * IAR的语法扩展总是显得很怪异; * Keil MDK默认不支持c99,需要加参数;<然后加或不加c99,程序体积是否有变动?> * IAR可以支持MISRA检查,好东西,不过这个检查真的好严格 * CCS的编译器?有些不记得了…… 以后RT-Thread的代码需要要求编译无警告才行了。目前内部实行,代码必须无警告(把警告即错误打开),必须有测试用例,必须有文档说明,这样代码才能被合并到代码仓库中。
MurphyZhao
认证专家
2020-01-08
这家伙很懒,什么也没写!
>有些编译器是挺坑的,或者编译器的世界其实都不同,都有很多自己的扩展 > >* VC++ 6.0,不支持很多东西; --- 这波分析很赞,了解了很多奇特特性。我 vs2015 测试正常,想测试 vc6.0,没装成功 :'( 不过 vc6.0 确实很老了,可以放弃了吧 代码无警告,必须有测试用例,这个想法很好,无测试,不谈代码 ;P。
pinxue
2020-01-08
这家伙很懒,什么也没写!
试试:编辑器 - 高级模式 - 扩展 - Markdown编辑器 [md] ```c int main(){ printf("hello, world"); return 0; } ```[/md]
MurphyZhao
认证专家
2020-01-08
这家伙很懒,什么也没写!
>试试:编辑器 - 高级模式 - 扩展 - Markdown编辑器 >[md] --- [md]刚刚试过了,引用头文件还存在问题。 ``` #include
#include
#define mkstr(var) (#var) int main(void) { rt_kprintf("hello rt-thread\n"); rt_kprintf(mkstr(hello rt-thread)); return 0; } ``` 还有这个 - 项目符号下的代码 ``` hello rt-thread hello rt-threadmsh /> ```[/md]
MurphyZhao
认证专家
2020-01-08
这家伙很懒,什么也没写!
>试试:编辑器 - 高级模式 - 扩展 - Markdown编辑器 >[md] --- 如果没有图片的话,markdown 也算是所见即所得了,富文本编辑器确实不太好改
撰写答案
登录
注册新账号
关注者
0
被浏览
1.8k
关于作者
MurphyZhao
这家伙很懒,什么也没写!
提问
6
回答
155
被采纳
0
关注TA
发私信
相关问题
1
RT-Thread内存和字符串相关函数与C语言自带的内存和字符串相关函数冲突问题
2
嵌入式RT-thread中初始化线程函数中(void *)entry的意义何在
3
cJSON parse 失败,请问怎么解决?
4
请教一个C语言顺序表的问题,不知道有没有大佬帮吗解答
5
小白请教,关于头文件引用,、、哪些场景需要引用它们?
6
使用中断有warn。。。。。。。。。。。
7
局部变量位置被编译器改写
8
RTthread 两个例程结合在一起会出现incompatible type for argument 2 of 'led_matrix_set_color'
9
有没有在单片机编程使用goto语句的?
10
请问如何读取另一个c文件中的温湿度数据啊
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
最新文章
1
使用百度AI助手辅助编写一个rt-thread下的ONVIF设备发现功能的功能代码
2
RT-Thread 发布 EtherKit开源以太网硬件!
3
rt-thread使用cherryusb实现虚拟串口
4
《C++20 图形界面程序:速度与渲染效率的双重优化秘籍》
5
《原子操作:程序世界里的“最小魔法单位”解析》
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
ota在线升级
UART
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
keil_MDK
rt_mq_消息队列_msg_queue
at_device
ulog
C++_cpp
本月问答贡献
踩姑娘的小蘑菇
7
个答案
3
次被采纳
a1012112796
13
个答案
2
次被采纳
张世争
9
个答案
2
次被采纳
rv666
5
个答案
2
次被采纳
用户名由3_15位
11
个答案
1
次被采纳
本月文章贡献
程序员阿伟
7
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
大龄码农
1
篇文章
2
次点赞
ThinkCode
1
篇文章
1
次点赞
Betrayer
1
篇文章
1
次点赞
回到
顶部
发布
问题
分享
好友
手机
浏览
扫码手机浏览
投诉
建议
回到
底部