1‘#’ 和 ‘##’ 属于预处理标记。‘#’ 和 ‘##’ 用于相似函数的宏定义中(或者简称为宏定义函数)。
2‘__VA_ARGS__’ 是 C99 引入的用于支持宏定义函数中使用可变参数。
在宏定义展开的时候,标记 ‘#’ 用于将 ‘#’ 后面的宏定义函数中的参数转化为对应的字符串。宏定义函数的参数与预处理标记 ‘#’ 之间出现的每个空格都会被删除,并删除第一个预处理标记以前和最后一个预处理标记以后的空白字符,可是宏定义函数参数中的空格会保留。php
其中,空参数转化为为空,即宏定义函数入参为空,那么展开的时候也为空。html
上面的这段话比较难理解,这里为了准确地传达其意义,咱们来看一个示例程序。nginx
在看到代码后,能够先猜猜可能的输出结果,若是你答对了,那就是真的会了!
注意,这里我基于 RT-Thread QEMU BSP 进行代码展现,代码真实编译经过,运行正常。git
请看如下代码:github
1#include <stdint.h>
2#include <rtthread.h>
3
4#define mkstr(var) (#var)
5
6int main(void)
7{
8 rt_kprintf("hello rt-thread\n");
9
10 rt_kprintf(mkstr(hello rt-thread));
11
12 return 0;
13}
请问:express
它能编译经过吗?api
它能输出什么内容?bash
答案:app
它能够正常编译经过ide
它输出的内容
1hello rt-thread
2hello rt-threadmsh />
从上面输出的信息能够看到,hello rt-thread
字符串被准确地输出到了控制台,可是没有增长回车换行。其中 msh />
字符串是 RT-Thread 控制台回显。
如上,代码 rt_kprintf(mkstr(hello rt-thread));
中的 hello rt-thread
在没有加引号的状况下,被转化成了字符串。
为示例程序 A 打印的字符串增长回车换行。
1#include <stdint.h>
2#include <rtthread.h>
3
4#define mkstr(var) (#var)
5
6int main(void)
7{
8 rt_kprintf("hello rt-thread\n");
9
10 rt_kprintf(mkstr(hello rt-thread\r\n));
11
12 return 0;
13}
有了示例 A 的基础,示例 B 那就是 soeasy,直接在原有的基础上增长 \r\n
转义字符便可输出回车换行。
输出结果以下:
1hello rt-thread
2hello rt-thread
3msh />
咱们在示例程序 B 中成功增长了回车换行的输出,可是你有没有想过一个问题,若是你又不少地方用到 mkstr
宏定义函数输出信息,那你是否是每个地方都要增长 \r\n
,这岂不是很累,有没有好的方法?
好方法固然有,下面介绍下程序中经常使用的方式,利用 C 语言相邻字符串自动拼接的特性(固然,这是编译器支持的)。代码以下:
1#include <stdint.h>
2#include <rtthread.h>
3
4#define mkstr(var) (#var"\r\n")
5
6int main(void)
7{
8 rt_kprintf("hello rt-thread\n");
9
10 rt_kprintf(mkstr(hello rt-thread));
11
12 return 0;
13}
以上代码在 #var
后面增长了一个字符串 \r\n
,咱们来看宏定义展开过程:
1-> rt_kprintf(mkstr(hello rt-thread));
2-> rt_kprintf("hello rt-thread""\r\n");
3-> rt_kprintf("hello rt-thread\r\n");
预处理标记 #
的基本用法已经展现完了,但怎么理解 “宏定义函数的参数与预处理标记 ‘#’ 之间出现的每个空格都会被删除,并删除第一个预处理标记以前和最后一个预处理标记以后的空白字符”?
请看下面的代码:
1#include <stdint.h>
2#include <rtthread.h>
3
4#define mkstr(var) ("aa" # var "bb")
5
6int main(void)
7{
8 rt_kprintf("hello rt-thread\n");
9
10 rt_kprintf(mkstr(hello rt-thread));
11
12 return 0;
13}
宏定义 mkstr(var) ("aa" # var "bb\r\n")
中的 # var
中间有两个空格,根据定义,#
号与宏定义函数参数 var
中间的两个空格会被删除,可是 var
参数中的 hello rt-thread
中的空格不会被删除。
继续,根据定义,宏定义 mkstr(var) ("aa" # var "bb\r\n")
中只有一个预处理标记 #
,其做为第一个和最后一个预处理标记,它前面和后面的空格都会被删除。
所以,以上代码预计输出结果为:
1hello rt-thread
2aahello rt-threadbb
3msh />
在实际操做时,操做符 #
被经常使用于枚举转字符串。如下代码截取自个人 FlexibleButton 按键库 的示例程序。
1#define ENUM_TO_STR(e) (#e)
2
3typedef enum
4{
5 USER_BUTTON_0 = 0,
6 USER_BUTTON_1,
7 USER_BUTTON_2,
8 USER_BUTTON_3,
9 USER_BUTTON_MAX
10} user_button_t;
11
12static char *enum_btn_id_string[] = {
13 ENUM_TO_STR(USER_BUTTON_0),
14 ENUM_TO_STR(USER_BUTTON_1),
15 ENUM_TO_STR(USER_BUTTON_2),
16 ENUM_TO_STR(USER_BUTTON_3),
17 ENUM_TO_STR(USER_BUTTON_MAX),
18};
‘##’ 是预处理拼接标记。在宏定义展开的时候,将 ‘##’ 左边的内容,与 ‘##’ 右边的内容拼接到一块儿。
注意,对于任何一种形式的宏定义,‘##’ 预处理标记都不该出如今替换列表的开头或结尾。
关于替换列表:
例如宏定义 ‘#define aa(x, y) (x##y)’ 后面的部分 ‘x##y’ 就是替换列表。
预处理拼接符 ##
经常使用于使用宏定义批量生成函数或者变量。
1#include <stdint.h>
2#include <rtthread.h>
3
4#define my_math(x, y) (x##e##y)
5
6int main(void)
7{
8 rt_kprintf("hello rt-thread\n");
9
10 printf("%e\r\n", my_math(3, 4));
11
12 return 0;
13}
以上代码是科学计数法的格式输出到控制台,输出内容以下:
1hello rt-thread
23.000000e+04
3msh />
用预处理拼接符 ##
批量生成函数或者变量。
如下代码截取自 RT-Thread finsh_api.h
,该段代码用于导出 Finsh 命令,代码以下所示:
1#define FINSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \
2 const char __fsym_##cmd##_name[] SECTION(".rodata.name") = #cmd; \
3 const char __fsym_##cmd##_desc[] SECTION(".rodata.name") = #desc; \
4 RT_USED const struct finsh_syscall __fsym_##cmd SECTION("FSymTab")= \
5 { \
6 __fsym_##cmd##_name, \
7 __fsym_##cmd##_desc, \
8 (syscall_func)&name \
9 };
该宏定义的应用,如 list_timer
命令,以下所示:
1FINSH_FUNCTION_EXPORT_CMD(list_timer, list_timer, list timer in system);
__VA_ARGS__
是在 C99 中增长的新特性。虽然 C89 引入了一种标准机制,容许定义具备可变数量参数的函数,可是 C89 中不容许这种定义可变数量参数的方式出如今宏定义中。C99 中加入了 __VA_ARGS__
关键字,用于支持在宏定义中定义可变数量参数,用于接收 ...
传递的多个参数。1
__VA_ARGS__
只能出如今使用了省略号的像函数同样的宏定义里。例如 #define myprintf(...) fprintf(stderr, __VA_ARGS__)
。
经过宏定义,将多个参数传递给函数,那么函数是如何解析不定参的呢?
这就须要使用标准库头文件 <stdarg.h>
中的三个宏,分别是 “va_start()”、“va_arg()”、“va_end()”,以及一个可变参类型 “va_list”,示例使用方式借用 RT-Thread 中的 rt_sprintf
的实现,代码以下所示:
1rt_int32_t rt_sprintf(char *buf, const char *format, ...)
2{
3 rt_int32_t n;
4 va_list arg_ptr;
5
6 va_start(arg_ptr, format);
7 n = rt_vsprintf(buf, format, arg_ptr);
8 va_end(arg_ptr);
9
10 return n;
11}
首先使用 “va_list” 类型定义一个变量 “arg_ptr”
而后调用 “va_start(arg_ptr, format);” 函数,第一个入参是 “va_list” 类型,第二个参数是 “rt_sprintf” 函数参数列表中的最后一个定参 “format”
而后,经过调用 “rt_vsprintf” 函数,根据 “format” 来解析不定参,并将结果存放到 “buf” 中
最后,使用 “va_end(arg_ptr);” 来释放不定参列表占用的资源
预处理标记 ‘#’ 用于将宏定义参数转化为字符串,所以 #__VA_ARGS__
会被展开为参数列表对应的字符串。
示例:
1#define showlist(...) put(#__VA_ARGS__)
2
3测试以下:
4showlist(The first, second, and third items.);
5showlist(arg1, arg2, arg3);
6
7输出结果分别为:
8The first, second, and third items.
9arg1, arg2, arg3
##__VA_ARGS__
是 GNU 特性,不是 C99 标准的一部分,C 标准不建议这样使用,但目前已经被大部分编译器支持。
标识符 ##__VA_ARGS__
的意义来自 ‘##’,主要为了解决一下应用场景:
1#define myprintf_a(fmt, ...) printf(fmt, __VA_ARGS__)
2#define myprintf_b(fmt, ...) printf(fmt, ##__VA_ARGS__)
3
4应用:
5myprintf_a("hello");
6myprintf_b("hello");
7
8myprintf_a("hello: %s", "world");
9myprintf_b("hello: %s", "world");
这个时候,编译器会报错,以下所示:
1applications\main.c: In function 'main':
2applications\main.c:26:57: error: expected expression before ')' token
3 #define myprintf_a(fmt, ...) printf(fmt, __VA_ARGS__)
4 ^
5applications\main.c:36:5: note: in expansion of macro 'myprintf_a'
6 myprintf_a("hello");
为何呢?
咱们展开 myprintf_a("hello");
以后为 printf("hello",)
。由于没有不定参,因此,__VA_ARGS__
展开为空白字符,这个时候,printf 函数中就多了一个 ‘,’(逗号),致使编译报错。而 ##__VA_ARGS__
在展开的时候,由于 ‘##’ 找不到链接对象,会将 ‘##’ 以前的空白字符和 ‘,’(逗号)删除,这个时候 printf 函数就没有了多余的 ‘,’(逗号)。