本文我们聊一聊一下宏和内联函数之间的关系。
inline
关键字进行修饰,具体怎样,敬请看下去吧在将内联函数之间前,我们再来聊聊有关宏的一些内容
下面是宏的申明方式:
#define name( parament-list ) stuff
//其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
注:① 参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
//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
,我们一一来分析一下
int ret = ADD(1, 2);
int ret = ADD(1, 2) * 3;
*
的优先级来得高,所以2会和后面的3先进行一个运算,这也就造成了最后结果的不同所以我们应该要像下面这样,在外层加上一个大括号,防止出现优先级的问题
#define ADD(x, y) (x + y)
int a = 10;
int b = 20;
int ret = ADD(a | b, a & b);
+
号来说,它的优先级高于&
按位与和|
按位或的,如果不清楚优先的话可以看看操作符汇总大全&
和|
()
,这样就不会出现问题了#define ADD(x, y) ((x) + (y));
;
号,对于分号来说是我们在写代码的时候作为一条语句结束的标志,但是对于宏来说也可以写分号吗?int ret = ADD(1, 2) * 3;
;
号,但是分号后面还有3要乘,这个时候其实就不对了,所以宏定义后是不可以加分号的#define ADD(x, y) ((x) + (y)) //✔
看了上面有关【宏】概念的一些回顾,我们来聊聊它的缺点所在
① 宏可能会带来运算符优先级的问题,导致程容易出现错。【加括号太麻烦了!!!】
② 宏是不能调试的【这点比较致命】
Windows环境下VS
Linux环境下gdb
③ 没有类型安全的检查【直接替换】
④ 有些场景下非常复杂,容易出错,不容易掌握
#define SWAP(n) num = (((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1))
了解了宏的缺点之后,我们再来瞧瞧它的优点,既然在一些场景被广泛使用,那一定也具有它的优点
① 宏常量
提高复用性和可维护性
#define n 500
② 宏函数
类型不固定,int、double、float都可以用
③ 宏函数
不用建立栈帧,因此不会产生消耗
好,以上就是本文所要讲的有关【宏】相关的所有内容,希望能唤起读者对这个知识点的回忆
接下去我们就正式来讲讲C++中的独有的【内联函数】
以
inline
修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率
inline int Add(int x, int y)
{
int z = x + y;
return z;
}
call
指令吗,哪来的不调用一说呢?call
指令了,编译器直接将内敛函数Add做了展开inline
关键字,就可以起到这种效果。非但如此,它还可以调试接下去我们再来介绍一下有关内敛函数的一些特性
有同学可能不是很理解空间换时间是什么意思,可以看看时空复杂度章节。不过要说明的一点是这里的空间不是内存,是编译后的程序,而【空间换时间】就会使得编译出来的可执行程序会变大
swap()
函数,内部有10行代码。在程序的1000个地方都会对其进行调用,那请问此时去使用内联函数和不使用内联函数的区别是什么?
swap()
函数当做普通的函数的话,我们知道在底层会进行一个call
指令再通过jump
指令跳转到这个函数所在地址然后执行这个函数,那每一个调用的地方都要跳转的话,就会跳转1000次,所以swap + 调用swap指令,合计是1000 + 10条指令swap()
函数的每一块地方,所以swap + 调用swap指令,合计是1000 * 10条指令在理解了一个函数设不设置内联函数的区别的时候,顺带来谈一谈这个目标文件体积变大的缘故
指令
,只是你们的指令数目不同而已。那我们都知道【苹果】这家公司系统是自己设计的,而且对于像芯片、硬件、软件各方面都领先于同行好,小插曲,我们回归正题,继续来讲讲内联函数的第二大特性 —— 【inline实现机制】
《C++prime》第五版关于inline的建议:
call
指令,而不是将其函数中的内容直接拷贝到程序调用处,即使是加了这个inline
关键字,似乎还是起不到内联函数的作用,这是为什么呢?1000 + 1000
条指令1000 * 1000
条指令,这就很恐怖了所以呢,加不加内敛是你的事,最终要不要把它真的展开变成内敛编译器说了算,所以我们在使用的时候一般是比较短小、调用频繁的函数(10几行以内)加上内联,其他就不加了
inline
内联函数的第三个特性,就是我们要注意内联函数的定义和声明不可以分开,导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
// F.h
#include
using namespace std;
inline void f(int i);
//--------------------------
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
//--------------------------
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
这种错误一般都是最后链接目标文件的时候除了问题,不是很清楚的可以看看程序的编译和链接
对于这些很复杂的符号func@@YAXH@Z
,是C++中的函数名修饰,可以看看探究函数重载的原理:函数名修饰
对于内联函数来说是这样,但是普通函数来说是怎么一步步地进行调用的呢?我们可以通过汇编来看看
call
的到底是谁的地址呢?F11
跳转到了一条叫做【jmp】的指令,此时你去观察这个地址,其实就是这条【jmp】的地址,在英语中jump
是跳的意思,那你可以将这条指令理解为一个中转站,call
指令会暂时跳到这条指令所在的地址,然后再通过这条指令去找需要调用函数的地址F10
的话就会通过这个函数的地址找到这个函数,此时就开始执行函数内部的逻辑,call
指令的下一条指令,这就算是一次函数调用的过程可以将它们放到一起来观察,就可以发现它们之间的逻辑非常紧密
如果对上面过程还是不太理解的可以看看反汇编深挖【函数栈帧】的创建和销毁
上面是对于普通函数的调用机制,那对于内联函数呢?也是这么去跳转吗?
可是呢,为什么会出现链接错误❌
func.h
这个头文件,但是在主调用接口中包含的头文件中只有函数的声明没有实现,此时只能在【链接】的时候展开了,但是在链接的时候因为只有声明所以只得到了函数名修饰后的地址。编译器便需要通过这个地址找到函数所在的位置,对于普通函数而言在这个时候就可以通过call
找过去了,但是对于内联函数而言,却无法做到,因为它并没有【call】和【jmp】这些指令,因此就造成了链接错误的现象那要如何去解决呢?
// F.h
#include
using namespace std;
inline void f(int i)
{
cout << i << endl;
}
//--------------------------
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
好,最后来总结一下本文所学习的内容
以上就是本文要介绍的所有内容,感谢您的阅读