C/C++ 中的宏/Macro

宏(Macro)本质上就是代码片段,通过别名来使用。在编译前的预处理中,宏会被替换为真实所指代的代码片段,即下图中 Preprocessor 处理的部分。

C/C++ 代码编译过程 - 图片来自 ntu.edu.sg

看我主页简介免费C++学习资源,视频教程、职业规划、面试详解、学习路线、开发工具

每晚8点直播讲解C++编程技术。

根据用法的不同,分两种,Object-like 和 Function-like。前者用于 Object 对象,后者用于函数方法。

C/C++ 代码编译过程中,可通过相应参数来获取到各编译步骤中的产出,比如想看被预处理编译之后的宏,使用 gcc 使加上 -E 参数。

$ gcc -E macro.c

宏的定义

通过 #define 指令定义一个宏。

#defineNAME_OF_MACRO value

比如,以下代码定义了一个名为 BUFFER_SIZE 的宏,指代 1024 这个数字。

#defineBUFFER_SIZE 1024

使用时,

foo= (char *) malloc (BUFFER_SIZE);

使用预处理器编译:

$ gcc -E test.c

编译结果:

foo= (char *) malloc (1024);

多行

宏的定义是跟随 #define 在一同一行内的,但可通过 反斜杠 \ 实现换行从而定义出多行的宏。

#include#defineGREETING_STR \"hello \

world"intmain(){printf(GREETING_STR);return0;}

多行的宏经过编译后会还原到一行中。

test.c

#include#defineGREETING_STR \"hello \

world"intmain(){printf(GREETING_STR); }

编译后:

intmain(){printf("hello world");return0;}

宏展开时的顺序

宏的展开是在处理源码时按照其出现位置进行的,如果宏定义有嵌套关系,也是层层进行展开,比如:

#include#defineGREETING_NAME"wayou"#defineGREETING"hello,"GREETING_NAMEintmain(){printf(GREETING);return0;}

首先遇到 GREETING ,将其展开成 GREETING_NAME "wayou" ,然后发现另一个宏 GREETING_NAME ,将其展开最后得到 "hello," "wayou" 。所以编译后的代码为:

intmain(){printf("hello,""wayou");return0;}

其展开的顺序并不是宏定义时的顺序,为了验证,可将上面示例代码中两个宏的定义调换一下,得到:

-#defineGREETING_NAME"wayou"#defineGREETING"hello,"GREETING_NAME+#defineGREETING_NAME"wayou"

再次编译查看产出,会发现没有区别,也不会报 GREETING 中所依赖的 GREETING_NAME 找不到的错。其实 #define 只是告诉编译器定义了这么个宏,而具体的求值,则是使用宏的地方才开始的。

像下面这样,当宏存在覆盖时,会以新的为准,其结果为 37。

#defineBUFSIZE 1020#defineTABLESIZE BUFSIZE#undefBUFSIZE#defineBUFSIZE 37

Object-like 宏

Object-like 类型的宏看起来就像普通的数据对象,故名。多用于数字常量的情形下。且宏名一般使用全大写形式方便识别。像上面示例中,都是 Object-like 的。

Function-like 宏

也可定义出使用时像是方法调用一样的宏,这便是 Function-like 类型的宏。

#definelang_init()c_init()lang_init()// 编译后c_init()

函数类型的宏只在以方法调用形式使用时才会被展开,即名称后加括号,否则会被忽略。当宏名和函数名重名时,这一策略就会显得有用了,比如:

externvoidfoo(void);#definefoo()/* optimized inline version */…  foo();  funcptr = foo;

这里 foo() 的调用会来自宏里面定义的那个函数,而 funcptr 会正确地指向函数地址,如果后者也被宏展开,则成了 funptr=foo() 显然就不对了。

函数类型的宏在定义时需注意,宏名与后面括号不能有空格,否则就是普通的 Object-like 类型对象。

#definelang_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"requires2arguments, butonly1givenmin(,,)    error→ macro"min"passed3arguments, but takes just2

字符化/Stringizing

如果函数宏中入参在字符串中,是不会被展开的,它就是普通的字符串字面量,这样的结果是符合预期的。

#definefoo(x) x,"x"foo(bar)        → bar,"x"

但如果确实想将入参展开成字符串,可在使用入参时,加上 # 前缀。

#defineWARN_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",就是拼接嘛。

宏拼接一般用在需要拼接的宏是来自宏参数的情况,其他情况,大可直接将两个宏写在一起即可,用不着 ## 指令。

考察下面这个场景,其中命令名重复出现:

structcommand{  char *name;void(*function) (void);};structcommandcommands[] ={  {"quit", quit_command },  {"help", help_command },  …};

通过定义宏配合拼接,可达到精简代码的目的:

#define COMMAND(NAME)  { #NAME, NAME ## _command }structcommand commands[] ={COMMAND(quit),  COMMAND (help),  …};

不定参数

像普通函数一样,函数类型的宏也可定义接收不定参数。

#defineeprintf(…)fprintf(stderr, __VA_ARGS__)

调用时,命名参数后面,包括逗号都会进入到 __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 4x= FOO;        → x =4;#undef FOOx= FOO;        → x = FOO;

两个宏相似的定义

满足以下条件时,我们认为两者是相似的:

类型相同,比如同为对象类型,或函数类型的宏

展开后各位置的符号(token)相同

如果是函数宏,入参相同

空白的不限但出现的位置相同

比如,下面这些是相似的:

#defineFOUR(2+2)#defineFOUR(2+2)#defineFOUR(2/* two */+2)

而下面这些则不然:

#defineFOUR(2+2)#defineFOUR(2+2)// 空白位置不一样 #defineFOUR(2*2)// 宏的内容不一样#defineFOUR(score,and,seven,years,ago) (2+2)// 入参不一样

宏重复定义时的表现

对于使用了 #undef 注销过的宏,再次定义同名的宏时,要求新定义的宏不与老的相似。

而如果说一个已经存在的宏,并没有注销,重复定义时,如果相似,则新的定义会忽略,如果不相似,编译器会报警告同时使用新定义的宏。这允许在多个文件中定义同一个宏。

看我主页简介免费C++学习资源,视频教程、职业规划、面试详解、学习路线、开发工具

每晚8点直播讲解C++编程技术。

你可能感兴趣的:(C/C++ 中的宏/Macro)