宏定义
概念
C/C++ 语言中的宏定义实现的是一个文本替换的功能,它使用#define
命令用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。
在预处理阶段
,编译器会把源代码中使用宏定义标识符的地方,按照宏定义内容替换成相应的文本。
宏定义仅仅是简单的文本替换,不做任何类型合法性检查。
宏定义有两种格式:
-
简单宏定义:
#define 标识符 记号序列
示例:
#define INT_MAX 2147483647
将使得预处理器把该标识符后续出现的各个实例用给定的记号序列替换。记号序列前后的空白符将被丢弃。 -
带参数的宏定义:
#define 标识符(标识符表opt) 记号序列
示例:
#define MAX(a,b) ((a>b)?(a):(b))
带有形式参数(由标识符表指定)的宏定义的第一个标识符与圆括号(
之间没有空格。记号序列前后的空白符也会被丢弃。
注意事项⚠️
宏定义没有函数的重载概念,只有覆盖。
也就是说如果你定义了两个标识符(宏名称)相同,但内容不同(参数列表、展开内容等)的宏, 后定义的宏将会覆盖前面的宏。
#define VAL 10000
#define VAL 20000
std::cout << "VAL is " << VAL; // 打印:VAL is 20000
宏定义的作用域
宏定义的作用域是从定义处到文件结尾处(与之对比🆚,using、typedef 等的作用域,则与变量定义的作用域相同)
有参宏定义的参数匹配
对于带参数的宏定义,宏定义内容部分中出现的参数,其前后必须出现分隔符(字母、数字、下划线以外的符号
,比如 空格、‘-’ ‘:’ 等) 才会被预处理当做参数,否则会被当做普通字符串原样保留。
#define CAT_STR1(str1, str2) str1str2
#define CAT_STR2(str1, str2) str1_str2
#define CAT_STR3(str1, str2) str1-str2
CAT_STR1(hello, world) // 展开结果: str1str2
CAT_STR2(hello, world) // 展开结果: str1_str2
CAT_STR3(hello, world) // 展开结果: hello-world
取消宏定义
#undef 标识符
用于取消标识符的预处理器定义。 将 #undef
应用于未知标识符(即未使用#define
指令定义的标识符)并不会导致错误。 示例: #undef INT_MAX
宏定义的特殊符号
C/C++ 中有几个特殊用途的符号: #,##,#@
- # : 将宏参数转换为字符串,相当于加双引号。
- ## : 用于拼接两个符号,这里的符号可以是宏定义参数,也可以是其他普通字符。
- #@ : 它将单字符标记符变换为单字符,即加单引号。
[!CAUTION]
#@ : gcc 和 clang 编译器并不支持;貌似只有 msvc 编译器支持。
#define GREETWORDS "good morning"
printf("%s\n", TO_STR(hello moto)); // 输出 hello moto
printf("%s\n", CAT_STR(GREET, WORDS)); // 输出 good morning
嵌套宏的展开
先讲一些宏嵌套的展开规则:
- 一般的展开规律像函数的参数一样:先展开参数,再分析函数,即由内向外展开
- 当宏中有#运算符的时候,不展开参数
- 当宏中有##运算符的时候,先展开函数,再分析参数
- ##运算符用于将参数连接到一起,预处理过程把出现在##运算符两侧的参数合并成一个 符号,注意不是字符串
嵌套宏展开规则的流程图如下:
举例说明:
#include <cstdio>
#define TO_STRING3(x) a_##x
#define TO_STRING2(x) #x
#define TO_STRING1(x) #x
#define TO_STRING(x) TO_STRING1(x)
#define PARAM(x) #x
#define ADDPARAM(x) INT_##x
int main(int argc, char* argv[]) {
// example-1
const char *str1 = TO_STRING(PARAM(ADDPARAM(1)));
printf("%s\n",str1); // 输出: "ADDPARAM(1)"
// example-2
const char *str2 = TO_STRING2(PARAM(ADDPARAM(1)));
printf("%s\n",str2); // 输出: PARAM(ADDPARAM(1))
// example-3
const char *str3 = TO_STRING(TO_STRING3(PARAM(ADDPARAM(1))));
printf("%s\n",str3); //输出: a_PARAM(INT_1)
return 0;
}
从左往右(由外向内)依次扫描,直到遇到满足流程图中条件 2和3 的宏之后,按照规则进行展开;然后进行下一次扫描,直到展开后的内容中不再含有任何宏为止。
Example-1 展开过程:
- 遇到 PARAM (包含 # ),展开后变为:TO_STRING(“ADDPARAM(1)”)
- 遇到 TO_STRING(当前最内一层宏), 展开后变为:TO_STRING1(“ADDPARAM(1)”)
- 遇到 TO_STRING1(当前最内一层宏), 展开后变为:““ADDPARAM(1)””
- 结束🔚
Example-2 展开过程:
- 遇到 TO_STRING2 (包含 # ),展开后变为:“PARAM(ADDPARAM(1)”
- 结束🔚
Example-3 展开过程:
- 遇到 TO_STRING2(包含 ##),展开后变为: TO_STRING(a_PARAM(ADDPARAM(1)))
- 遇到 ADDPARAM(包含 ##), 展开后变为:TO_STRING(a_PARAM(INT_1))
- 遇到 TO_STRING(最内一层宏),展开后变为: TO_STRING1(a_PARAM(INT_1))
- 遇到 TO_STRING1(最内一层宏),展开后变为: “a_PARAM(INT_1)”
- 结束🔚
[!CAUTION]
注意:嵌套宏的展开规则与编译器有关,不同的编译器可能对同一个嵌套宏展开不同。
可变参数宏
定义可变参数宏
两种方法:
- C99 中加入了
__VA_ARGS__
关键字,用于支持在宏定义中定义可变数量参数,用于接收...
传递的多个参数。
__VA_ARGS__
只能出现在使用了省略号的像函数一样的宏定义里。
#define MY_VA_PRINT1(fmt, ...) printf(fmt, __VA_ARGS__)
MY_VA_PRINT1("my_va_print1: today`s date is %d - %d - %d", 2024,9,19);
// 打印 my_va_print1: today`s date is 2024 - 9 - 19
- 现在也支持使用具名可变参数宏,在 名称后面加上
...
表示其为可变参数宏。
#define MY_VA_PRINT2(fmt, many_args...) printf(fmt, many_args)
MY_VA_PRINT1("my_va_print2: Now clock time is %d:%d", 22,36);
// 打印 my_va_print2: Now clock time is 22:36
两种可变参数宏不能混用。
带 ‘#’ 的可变参数宏
预处理标记 ‘#’ 用于将宏定义参数转化为字符串,因此 #__VA_ARGS__
会被展开为参数列表对应的字符串。
#define SHOW_VA_ARGS(...) printf("%s", #__VA_ARGS__)
SHOW_VA_ARGS(The first, second, and third items.);
// 打印:The first, second, and third items. 包含参数之间的comma逗号
带 ## 的可变参数宏
##__VA_ARGS__
是 GNU 编译器的特性,标识符 ##__VA_ARGS__
的意义来自 ‘##’,主要为了解决当传入的可变参数为空时,多余的符号导致编译器报错的问题。
#define myprintf_a(fmt, ...) printf(fmt, __VA_ARGS__)
#define myprintf_b(fmt, ...) printf(fmt, ##__VA_ARGS__)
// 应用:
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 函数就没有了多余的 ‘,’(逗号)。
[!NOTE]
本文提到的有些宏定义、展开等特性,可能依赖于特定的编译器,不属于C/C++标准。不同编译器的支持情况不同。