Chapter2 C与C++——2.4 高级宏操作

2.4 高级宏操作

“魔术是什么?魔术是错觉。但是错觉是为了给人带来快乐,娱乐和灵感。这是关于信仰、信念、信任。脱离了这些属性,魔术就不再是一种艺术了。”  

                                ——《惊天魔盗团》  

之前我们把宏理解为编译时的简单文本替换,实际上还有很多更神奇的宏操作,他们像程序里的魔术师一样,让人惊讶,神往,想一探究竟。但请切记:“The closer you look, the less you see。”

2.4.1 条件编译

宏的第一个魔术,便是保护唯一性,常常在头文件里看到类似下面的内容:

/**
 * @file some.h
 */
#ifndef SOME_H
#define SOME_H

/* some codes... */

#endif

在这样的文件中,也许我们会 tyoedef 一些类型,也有可能 define 一些宏,例如: “#define PI 3.1415”。然后在项目的其他文件里,会发现有好多:“#include “some.h””的地方,我们肆无忌惮的使用,甚至在某个文件中两次或者多次包含了 some.h 这个文件。在这种情况下,是否意味着 PI 被重复定义了多次呢?

如果 PI 在某个文件中被重复定义,那么编译器会报错。当我们实际编译这样的项目时,发现并没有错误。这说明:在一个文件中,两次 include 了 some.h,但是其内容只被包含入了一次。

这是怎么做到的?当了解了条件编译之后,你就会理解了。

C/C++中的下列宏可以控制编译器的行为:

#if 整形常量表达式1
    程序段1
#elif 整形常量表达式2
    程序段2
#else
    程序段3
#endif


#if defined 宏名1
    程序段1
#elif defined 宏名2
    程序段2
#else
    程序段3
#endif


#ifdef 宏名
    程序段1
#else
    程序段2
#endif


#ifndef 宏名
    程序段1
#else
    程序段2
#endif

上面的宏告诉编译器,只在满足条件的情况下去编译特定的代码。不满足条件的话,对应代码不被编译。

现在,我们回来解析 some.h 文件:如果没有定义 SOME_H 这个宏,则定义一个 SOME_H 这个宏,并且声明“/* some codes… */”中的内容,如定义了 PI 这个宏。如果谋文件两次以上包含了 some.h 这个文,则从第二次开始,会发现之前已经定义过 SOME_H 宏,因此后续的判断都会失败,从而保证 some.h 中的内容只被包含一次。

注意:some.h 中的保护范围实际上是从 #ifdef 开始到 #endif 结束。在此范围之外的内容仍会被多次包含。  

条件编译除了被用在头文件中用于避免重复引用外,还被经常被用于增加程序的通用性和可移植性上。如硬件平台有部分差异,造成项目的少部分代码有所不同,此时可以使用条件编译,将有差异的代码放到不同的条件下。对应不同硬件,可以人为改变编译条件,确保编译出对应硬件平台的程序。

2.4.2 宏函数

接下来让我们要Show一些高级魔术。

#define XN2(x, y) ((x##y)*(x##y))  // 这是一个宏函数, 符号 ## 起到字符串链接的作用.
int cr0=2, cr1=3, cr2=4;

printf("%d", XN2(cr, 0));   // print 4.
printf("%d", XN2(cr, 1));   // print 9.
printf("%d", XN2(cr, 2));   // print 16.

你看到的没错,宏是可以接收参数的,在编译时宏的形参被替换为实参。上述宏中使用了 ## 作为字符串链接符,因此第一个 printf 处被替换为:((cr0)*(cr0))。

这里有个长一点儿的宏函数:

#define PI  3.1415

#define Volume(v, s, r, h) \
do { \
    (s)=PI*(r)*(r); \
    (v)=(s)*(h); \
}while(0)

通过 C/C++ 的换行符,我们将一个比较长的宏函数写成了多行。这段程序用于计算圆柱体体积。问题是上面的 do…while(0) 是用来做什么的?

上面这种写法可以避免在宏替换时出现 if…else 不匹配的情况:

// 下面 a, v, s 为预先定义好的变量,注意 v 和 s 的值会发生变化.
if(a)
    Volume(v, s, 5, 10);
else
    Volume(v, s, 3, 15);

可以想象,上面的宏 Volume 如果没有被 do…while(0) 包围,则 if 后面有多条语句且没有形成语句块,这就造成 if 没有与后面的 else 配对,出现语法错误。

注意:与普通函数不同,宏函数并不对参数进行类型检查。并且宏参数是基于替换原则,而非函数参数的值传递,这意味着宏中参数的变化直接影响实际参数的值。  

2.4.3 编译器内置宏

宏这个魔术师自带一些天然属性,这就是编译器内置宏,这些宏不需要在程序中定义,而可以直接使用:

宏名描述
FILE编译时被替换问当前源码文件名
LINE编译时被替换问当前所在行的行号
FUNCTION编译时被替换为当前所在函数的函数名
DATE编译时被替换为编译的日期
TIME编译时被替换为编译的时间
VA_ARGS可变宏参数,与宏参数中的 … 配对使用

以上内置宏常被用于程序的调试工作:

#define DEBUG_LEVEL 1

#define DEBUG(level, fmt, ...) \
do { \
    if(level<DEBUG_LEVEL) \
        printf(fmt, __VA_ARGS__); \
}while(0)

DEBUG(0, "file:%s, func:%s, line:%d\n", __FILE__, __FUNCTION__, __LINE__);  // 打印当前文件名, 函数名和行号.
DEBUG(0, "compile date:%s, time:%s.\n", __DATE__, __TIME__);    // 打印编译日期和时间.
DEBUG(1, "Hello.\n");   // 在运行时, 此行不会被打印.

练习

已知双向链表表头结构如下:

struct list_head {
    struct list_head *next, *prev;
};

使用宏创建并初始化一个双向链表。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lionchan187

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值