宏(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 在一同一行内的,但可通过 反斜杠 \ 实现换行从而定义出多行的宏。

#include <stdio.h>
#define GREETING_STR \
"hello \
world" int main() {

printf(GREETING_STR);

return 0;

}

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

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 = yx + 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__)

调用时,命名参数后面,包括逗号都会进入到 __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 注销过的宏,再次定义同名的宏时,要求新定义的宏不与老的相似。

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

相关资源

C/C++ 中的宏/Macro的更多相关文章

  1. uboot中的中断macro宏

    目录 uboot中的中断macro宏 引入 内存分配 流程概览 普通中断 保存现场 中断函数打印具体寄存器 恢复现场 软中断 空间获取 保存现场 附录速记 疑惑待解 title: uboot中的中断m ...

  2. C中的宏

    1. 简单宏定义 简单的宏定义有如下格式: [#define指令(简单的宏)] #define  标识符替换列表 替换列表是一系列的C语言记号,包括标识符.关键字.数.字符常量.字符串字面量.运算符和 ...

  3. flask第30篇——宏macro和import标签

    宏是Jinja2特有的,像Django则没有这个. 先新建一个项目macroDemo: 然后在templates文件夹中新建index.html文件,并在代码中返回渲染后的文件: 然后回到index. ...

  4. flask中的宏

    对于flask中的宏编程.我们使用 macro 来对宏起个名称 宏编程 对于我们来说是减少了代码的重用.以及简化了标签的操作,对与开发效率有很大的提升, 在html中.相信大多数都用到了.input ...

  5. zabbix上的宏(macro)介绍

    宏:macro,预设的文本替换模式: 宏是一种抽象概念(Abstraction),它根据一些列预定义的规则替换一定的文本模式,而解释或编译器在遇到宏时会自动进行这一模式替换.类似地,zabbix基于宏 ...

  6. Makefile中用宏定义进行条件编译(gcc -D)/在Makefile中进行宏定义-D【转】

    本文转载自:http://blog.csdn.net/maopig/article/details/7230311 在源代码里面如果这样是定义的:#ifdef   MACRONAME//可选代码#en ...

  7. Flask基础(15)-->模板代码的复用【宏(Macro)、继承(Block)、包含(include)】

    宏 对宏(macro)的理解: 把它看作 Jinja2 中的一个函数,它会返回一个模板或者 HTML 字符串 为了避免反复地编写同样的模板代码,出现代码冗余,可以把他们写成函数以进行重用 需要在多处重 ...

  8. js中的宏任务与微任务

    如果你已经知道了js中存在宏任务和微任务,那么你一定已经了解过promise了.因为在js中promise是微任务的一个入口. 先来看一道题: setTimeout(function(){ conso ...

  9. C语言的宏macro的使用

    C's Macro Introduction 1.The Connect Macros: ## 这是一个预处理连接符,这个操作符主要用来将两个符号连接成为一个完整的宏符号.通过下面的代码,可以看到其具 ...

随机推荐

  1. 服务器上无法调用Office组件的解决方法(HRESULT:0X800A03EC异常)

    HRESULT:0X800A03EC的异常,经过多番查找,终于找到了解决方法,在 Windows 2008 中, 如果以 SYSTEM 用户跑, 系统会去寻找 SYSTEM 这个用户的 Profile ...

  2. 运用Java调用Kettle Job和Trans(带参数)

    1.首先创建一个kettle trans 2.对表输入进行编辑 ${dateNow}为要传入的参数. 3.代码编写 首先需要把包导入 import org.pentaho.di.core.Kettle ...

  3. idea 几个常用的设置

    一.主题的背景

  4. 吃透Python上下文管理器

    什么是上下文管理器? 我们常见的with open操作文件,就是一个上下文管理器.如: with open(file, 'rb') as f: text = f.read() 那上下文管理器具体的定义 ...

  5. Android Studio 安装教程

    前言 前段时间周围有很多认识的人学习Android,看蛮多人在装Android Studio,然而看他们装的过程不是那么顺利?然后也有高中同学来问我,于是乎就自己也试着去装了下,也方便日后学习Andr ...

  6. 权值初始化 - Xavier和MSRA方法

    设计好神经网络结构以及loss function 后,训练神经网络的步骤如下: 初始化权值参数 选择一个合适的梯度下降算法(例如:Adam,RMSprop等) 重复下面的迭代过程: 输入的正向传播 计 ...

  7. hadoop伪分布模式的配置和一些常用命令

    大数据的发展历史 3V:volume.velocity.variety(结构化和非结构化数据).value(价值密度低) 大数据带来的技术挑战 存储容量不断增加 获取有价值的信息的难度:搜索.广告.推 ...

  8. Android 上下文菜单 ContextMenu

    public class MainActivity extends Activity { private ListView listView; @Override protected void onC ...

  9. zabbix服务深入

    第1章 Grafana自定义图形 1.安装grafana [root@m01 /data/soft]# wget https://dl.grafana.com/oss/release/grafana- ...

  10. springcloud~配置中心~对敏感信息加密

    简介 RSA非对称加密有着非常强大的安全性,HTTPS的SSL加密就是使用这种方法进行HTTPS请求加密传输的.因为RSA算法会涉及Private Key和Public Key分别用来加密和解密,所以 ...