CC++中宏Macro的深⼊讲解
前⾔
宏(Macro)本质上就是代码⽚段,通过别名来使⽤。在编译前的预处理中,宏会被替换为真实所指代的代码⽚段,即下图中Preprocessor 处理的部分。
C/C++ 代码编译过程 - 图⽚来⾃ ntu.edu.sg
根据⽤法的不同,分两种,Object-like 和 Function-like。前者⽤于 Object 对象,后者⽤于函数⽅法。
C/C++ 代码编译过程中,可通过相应参数来获取到各编译步骤中的产出,⽐如想看被预处理编译之后的宏,使⽤ gcc 使加上 -E 参数。
$ gcc -E macro.c
宏的定义
通过 #define 指令定义⼀个宏。
#define NAME_OF_MACRO value
⽐如,以下代码定义了⼀个名为 BUFFER_SIZE 的宏,指代 1024 这个数字。
#define BUFFER_SIZE 1024
使⽤时,
foo = (char *) malloc (BUFFER_SIZE);
使⽤预处理器编译:
$ gcc -E test.c
编译结果:
foo = (char *) malloc (1024);
多⾏
宏的定义是跟随 #define 在⼀同⼀⾏内的,但可通过反斜杠 \ 实现换⾏从⽽定义出多⾏的宏。
多⾏的宏经过编译后会还原到⼀⾏中。
test.c
#include <stdio.h>
#define GREETING_STR \
"hello \
world"
int main() { printf(GREETING_STR); }
编译后:
int main() {
printf("hello world");
return 0;
}
宏展开时的顺序
宏的展开是在处理源码时按照其出现位置进⾏的,如果宏定义有嵌套关系,也是层层进⾏展开,⽐如:
#include <stdio.h>
#define GREETING_NAME "wayou"
#define GREETING "hello," GREETING_NAME
int main() {
printf(GREETING);
return 0;
}
⾸先遇到 GREETING,将其展开成 GREETING_NAME "wayou",然后发现另⼀个宏 GREETING_NAME,将其展开最后得到 "hello," "wayou"。所以编译后的代码为:
int main() {
printf("hello," "wayou");
return 0;
}
其展开的顺序并不是宏定义时的顺序,为了验证,可将上⾯⽰例代码中两个宏的定义调换⼀下,得到:
-#define GREETING_NAME "wayou"
#define GREETING "hello," GREETING_NAME
+#define GREETING_NAME "wayou"
再次编译查看产出,会发现没有区别,也不会报 GREETING 中所依赖的 GREETING_NAME 不到的错。其实 #define 只是告诉编译器定义了这么个宏,⽽具体的求值,则是使⽤宏的地⽅才开始的。
像下⾯这样,当宏存在覆盖时,会以新的为准,其结果为 37。
#define BUFSIZE 1020
#define TABLESIZE BUFSIZE
#undef BUFSIZE
#define BUFSIZE 37
Object-like 宏
Object-like 类型的宏看起来就像普通的数据对象,故名。多⽤于数字常量的情形下。且宏名⼀般使⽤全⼤写形式⽅便识别。像上⾯⽰例中,都是 Object-like 的。
Function-like 宏
也可定义出使⽤时像是⽅法调⽤⼀样的宏,这便是 Function-like 类型的宏。
#define lang_init() c_init()
lang_init()
// 编译后
c_init()
函数类型的宏只在以⽅法调⽤形式使⽤时才会被展开,即名称后加括号,否则会被忽略。当宏名和函数名重名时,这⼀策略就会显得有⽤了,⽐如:
extern void foo(void);
#define foo() /* optimized inline version */
…
foo();
funcptr = foo;
这⾥ foo() 的调⽤会来⾃宏⾥⾯定义的那个函数,⽽ funcptr 会正确地指向函数地址,如果后者也被宏展开,则成了
funptr=foo() 显然就不对了。
函数类型的宏在定义时需注意,宏名与后⾯括号不能有空格,否则就是普通的 Object-like 类型对象。
#define lang_init () c_init()
lang_init()
// 编译后:
() c_init()()
宏的参数
函数类型的宏,可以像正常函数⼀样指定⼊参,⼊参需为逗号分隔合法的 C 字⾯量。
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
x = min(a, b); → x = ((a) < (b) ? (a) : (b));
y = min(1, 2); → y = ((1) < (2) ? (1) : (2));
z = min(a + 28, *p); → z = ((a + 28) < (*p) ? (a + 28) : (*p));
⼊参中的括号
⼊参中只需要括号对称,但不要求⽅括号或花括号成对出现,所以下⾯的代码:
macro (array[x = y, x + 1])
其⼊参实际为 array[x = y 和 x + 1]。
⼊参的展开
⼊参本质上也是宏,对象类型的宏,在函数宏展⽰时,这些参数也被展⽰到了函数宏的函数体⾥。
min (min (a, b), c)
⾸先被展开成:
min (((a) < (b) ? (a) : (b)), (c))
然后进⼀步展开成(此处换⾏为⽅便阅读,实际编译后没有):
((((a) < (b) ? (a) : (b))) < (c)
(((a) < (b) (a) : (b)))
: (c))
参数的缺省
函数宏在使⽤时其⼊参可缺省,但不能全部缺省,⾄少提供⼀个⼊参。
min(, b) → (( ) < (b) ? ( ) : (b))
min(a, ) → ((a ) < ( ) ? (a ) : ( ))
min(,) → (( ) < ( ) ? ( ) : ( ))
min((,),) → (((,)) < ( ) ? ((,)) : ( ))
min() error→ macro "min" requires 2 arguments, but only 1 given
min(,,) error→ macro "min" passed 3 arguments, but takes just 2
字符化/Stringizing
如果函数宏中⼊参在字符串中,是不会被展开的,它就是普通的字符串字⾯量,这样的结果是符合预期的。
#define foo(x) x, "x"
foo(bar) → bar, "x"
但如果确实想将⼊参展开成字符串,可在使⽤⼊参时,加上 # 前缀。
#define WARN_IF(EXP) \
do { if (EXP) \
fprintf (stderr, "Warning: " #EXP "\n"); } \
while (0)
WARN_IF (x == 0);
→ do { if (x == 0)
fprintf (stderr, "Warning: " "x == 0" "\n"); } while (0);
此处 #EXP 在字符串中会被正确展开。What's more, 如果这⾥的 x 也是宏,那只会在 if 语句中进⾏展开。
拼接
通过 ## 可将两个宏展开成⼀个,即将两者进⾏了拼接,这种操作叫 "token pasting",或 "token concatenation",就是拼接嘛。
宏拼接⼀般⽤在需要拼接的宏是来⾃宏参数的情况,其他情况,⼤可直接将两个宏写在⼀起即可,⽤不着 ## 指令。
考察下⾯这个场景,其中命令名重复出现:
struct command
{
char *name;
void (*function) (void);
};
struct command commands[] =
{
{ "quit", quit_command },
{ "help", help_command },
…
};
通过定义宏配合拼接,可达到精简代码的⽬的:
#define COMMAND(NAME) { #NAME, NAME ## _command }
struct command commands[] =
{
COMMAND (quit),
COMMAND (help),
…
};
不定参数
像普通函数⼀样,函数类型的宏也可定义接收不定参数。
#define eprintf(…) fprintf (stderr, __VA_ARGS__)
wa字符串是什么调⽤时,命名参数后⾯,包括逗号都会进⼊到 __VA_ARGS__ 关键字当中。但 C++ 中还⽀持对这些参数命名从⽽不⽤__VA_ARGS__。
eprintf ("%s:%d: ", input_file, lineno)
// 编译后:
fprintf (stderr, "%s:%d: ", input_file, lineno)
C++ 中可这么写:
#define eprintf(args…) fprintf (stderr, args)
不定参数与命名参数混合的情况
不定参数为命名参数后⾯省略的部分。
#define eprintf(format, …) fprintf (stderr, format, __VA_ARGS__)
预设的宏
标准库及编译器中预设了⼀些有⽤的宏,可以在查阅。
取消和重置宏
当某个宏不再使⽤时,可通过 #undef 将取注销掉。#undef 后紧跟宏名,后⾯不要跟其他东西,即使是函数类型的宏。#define FOO 4
x = FOO; → x = 4;
#undef FOO
x = FOO; → x = FOO;
两个宏相似的定义
满⾜以下条件时,我们认为两者是相似的:
类型相同,⽐如同为对象类型,或函数类型的宏
展开后各位置的符号(token)相同
如果是函数宏,⼊参相同
空⽩的不限但出现的位置相同
⽐如,下⾯这些是相似的:
#define FOUR (2 + 2)
#define FOUR (2 + 2)
#define FOUR (2 /* two */ + 2)
⽽下⾯这些则不然:
#define FOUR (2 + 2)
#define FOUR ( 2+2 ) // 空⽩位置不⼀样
#define FOUR (2 * 2) // 宏的内容不⼀样
#define FOUR(score,and,seven,years,ago) (2 + 2) // ⼊参不⼀样
宏重复定义时的表现
对于使⽤了 #undef 注销过的宏,再次定义同名的宏时,要求新定义的宏不与⽼的相似。
⽽如果说⼀个已经存在的宏,并没有注销,重复定义时,如果相似,则新的定义会忽略,如果不相似,编译器会报警告同时使⽤新定义的宏。这允许在多个⽂件中定义同⼀个宏。
相关资源
gcc - Macros
Standard C++ Library reference
3.7.1 Standard Predefined Macros
总结
以上就是这篇⽂章的全部内容了,希望本⽂的内容对⼤家的学习或者⼯作具有⼀定的参考学习价值,谢谢⼤家对的⽀持。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论