时间:2021-05-20
前言
宏(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_NAMEint 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 37Object-like 宏
Object-like 类型的宏看起来就像普通的数据对象,故名。多用于数字常量的情形下。且宏名一般使用全大写形式方便识别。像上面示例中,都是 Object-like 的。
Function-like 宏
也可定义出使用时像是方法调用一样的宏,这便是 Function-like 类型的宏。
#define lang_init() c_init()lang_init()// 编译后c_init()函数类型的宏只在以方法调用形式使用时才会被展开,即名称后加括号,否则会被忽略。当宏名和函数名重名时,这一策略就会显得有用了,比如:
extern void foo(void);#define foo() … 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 givenmin(,,) 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__)调用时,命名参数后面,包括逗号都会进入到 __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;两个宏相似的定义
满足以下条件时,我们认为两者是相似的:
比如,下面这些是相似的:
#define FOUR (2 + 2)#define FOUR (2 + 2)#define FOUR (2 + 2)而下面这些则不然:
#define FOUR (2 + 2)#define FOUR ( 2+2 ) // 空白位置不一样 #define FOUR (2 * 2) // 宏的内容不一样#define FOUR(score,and,seven,years,ago) (2 + 2) // 入参不一样宏重复定义时的表现
对于使用了 #undef 注销过的宏,再次定义同名的宏时,要求新定义的宏不与老的相似。
而如果说一个已经存在的宏,并没有注销,重复定义时,如果相似,则新的定义会忽略,如果不相似,编译器会报警告同时使用新定义的宏。这允许在多个文件中定义同一个宏。
相关资源
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
本文实例讲解了C++中基类和派生类之间的转换。对于深入理解C++面向对象程序设计有一定的帮助作用。此处需要注意:本文实例讲解内容的前提是派生类继承基类的方式是公
C语言中const和C++中的const区别详解C++的const和C语言的#define都可以用来定义常量,二者是有区别的,const是有数据类型的常量,而宏
NULL对于学习过C/C++语言的朋友,对NULL一定很熟悉吧?这就是在C/C++中的空指针。在C语言中,NULL是无类型的,只是一个宏,它代表空。我们不研究C
本文主要记录了C++中的inline函数,也就是内联函数,主要记录了以下几个问题:一、C++为什么引入inline函数?主要目的:用它代替C语言中表达式形式的宏
1、导出C函数以用于C或C++的项目如果使用C语言编写的DLL,希望从中导出函数给C或C++的模块访问,则应使用__cplusplus预处理器宏确定正在编译的语