一、前言
本文我们聊一聊一下宏和内联函数之间的关系。
- 在预处理章节,我们重点介绍了什么是宏,以及宏和函数之间的区别,还记得下面这张图吗,我们对宏和函数之间做了一个很细致的分析:mag:
- 那在本文中,我们来说的就不是普通的函数了,它叫做【内联函数】,是C++里面独有的,由
inline
关键字进行修饰,具体怎样,敬请看下去吧👇
二、宏的优缺点分析
在将内联函数之间前,我们再来聊聊有关宏的一些内容
1、概念回顾
下面是宏的申明方式:
#define name( parament-list ) stuff //其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
//其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
注:① 参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
- 在学习了C语言后,如果我不带你去回顾这一些,那你是否能立马写出一个宏来呢,例如:写一个用于求和的宏函数
- 那我相信对于很多同学来说都会措手不及,答案会像是下面这样千奇百怪🤣
//1. #define Add(int x, int y) return x + y; //2. #define ADD(x, y) x + y //3. #define ADD(x, y) (x + y) //4. #define ADD(x, y) ((x) + (y)); //5. #define ADD(x, y) ((x) + (y))
- 那究竟哪一个对的呢?首先第一个肯定是错误的,因为我说了这是【宏】,而不是函数,这样子是函数的写法。
后面四个的主要区别就在于后面的
stuff
,我们一一来分析一下
- 首先若是写成下面这样,传入1给到x,传入2给到y,此时去我们去Linux环境下看看预处理之后会发生什么
int ret = ADD(1, 2);
- 可以看到很明显进行了一个【宏替换】
- 若此时我将调用的函数后面再乘上一个3呢,这会发生什么?
int ret = ADD(1, 2) * 3;
- 可以观察到此时在预处理阶段也是直接进行了一个替换,不过仔细观察就可以发现,由于
*
的优先级来得高,所以2会和后面的3先进行一个运算,这也就造成了最后结果的不同
所以我们应该要像下面这样,在外层加上一个大括号,防止出现优先级的问题
#define ADD(x, y) (x + y)
- 可是这样真的就可以了吗?若此时我向ADD宏函数传入下面这样的参数呢?会发生什么?
int a = 10; int b = 20; int ret = ADD(a | b, a & b);
- 编译器还是一样会去做傻傻的替换,但是这个时候我们又得注意优先级的问题了,对于
+
号来说,它的优先级高于&
按位与和|
按位或的,如果不清楚优先的话可以看看操作符汇总大全 - 所以中间的【b】和【a】会先进行结合,然后再去算
&
和|
- 如果要防止这种表达式的传入而造成的优先级问题,可以对内部的形参也加上一个括号
()
,这样就不会出现问题了
#define ADD(x, y) ((x) + (y));
- 可是呢,有的同学虽然想到了这一点,但是却在最后面加了一个
;
号,对于分号来说是我们在写代码的时候作为一条语句结束的标志,但是对于宏来说也可以写分号吗? - 将原先的传参继续替换成下面这样来试试
int ret = ADD(1, 2) * 3;
- 继续通过预处理之后的结果去进行观察,就可以发现在进行宏替换之后原先的语句中出现了
;
号,但是分号后面还有3要乘,这个时候其实就不对了,所以==宏定义后是不可以加分号的==
- 最后这一种才是最正确的写法
#define ADD(x, y) ((x) + (y)) //✔
2、宏的缺点
看了上面有关【宏】概念的一些回顾,我们来聊聊它的缺点所在
① 宏可能会带来运算符优先级的问题,导致程容易出现错。【加括号太麻烦了!!!】
- 这一点相信你在看了上一小节的叙述之后一定是感同身受,只是完成一个两数相加的功能,就需要加上这么多括号了,若是再复杂一些的功能,那岂不是要加很多了🙄
② 宏是不能调试的【这点比较致命👈】
- 可以观察到,无论是在哪个平台下,对于宏来说都是无法调试的
Windows环境下VS
Linux环境下gdb
③ 没有类型安全的检查【直接替换】
- 可以看到,无论我传入何种类型的参数,都不会出现问题。这点其实说明了宏对于类型的检查是不严谨的
④ 有些场景下非常复杂,容易出错,不容易掌握
- 之前我有写过一篇文章叫做使用宏将一个整数的二进制位的奇数位和偶数位交换。为了实现这个功能我写了一个这样的宏函数
#define SWAP(n) num = (((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1))
- 乍眼一看,就感觉非常复杂,都是一些二进制的位运算,若是没有对这一块很熟悉的话,就很容易写错,因此说对于一个宏来说其实要实现一些功能的时候会非常繁琐
3、宏的优点
了解了宏的缺点之后,我们再来瞧瞧它的优点,既然在一些场景被广泛使用,那一定也具有它的优点💡
① 宏常量
提高复用性和可维护性
- 这一点很好理解,也就是我们平常用得最多的,一般会将频繁使用到的一些数字定义成一个宏,这样就不需要每次写数字了,直接写这个宏所定义的变量即可
- 而且在修改的时候也会很方便,只需要修改一下宏定义处即可,实现一改多改【这一点在项目里面还是比较常用的】
#define n 500
② 宏函数
类型不固定,int、double、float都可以用
- 这一点其实我们在讲缺点的时候也有提及,虽然类型检查得不严谨,但真正用起来还是很香的,这很像是我们之前说到过的函数重载,虽然函数名相同,但是可以传入不同的参数实现不同的运算
③ 宏函数
不用建立栈帧,因此不会产生消耗
- 刚才我们有观察过,对于【宏】来说是不能调试的,因为它根本就不是一个真正意义上的函数,只是具备函数的功能罢了。既然不是函数的话也就不会在栈上建立函数栈帧,那也就不会有过多的消耗
好,以上就是本文所要讲的有关【宏】相关的所有内容,希望能唤起读者对这个知识点的回忆