三、inline内联函数
接下去我们就正式来讲讲C++中的独有的【内联函数】
1、概念
以
inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率
- C++的祖师爷呢,认为【宏】存在一定的缺陷,但是呢也有它的好处,也就是直接做替换,不需要开辟函数栈帧,所以在C++中就出现了内敛函数这么一个东西
inline int Add(int x, int y) { int z = x + y; return z; }
- 但是它真不会去进行函数调用吗?我们到VS中来看看
- 那此时就又同学疑惑了🤨这不还是会有
call指令吗,哪来的不调用一说呢? - 因为我们还需要去做一些配置💻
- 然后我们再去观察一下汇编就可以发现不存在
call指令了,编译器直接将内敛函数Add做了展开
- 所以C++中我们是不推荐用宏的,因为有内联函数这个特性,即保留了宏的优点,无需调用函数建立栈帧,而且还修复了宏的缺陷,不再需要将内容写得那么复杂,写成日常的函数形式即可,只需要在前面加上一个
inline关键字,就可以起到这种效果。非但如此,它还可以调试:computer:
接下去我们再来介绍一下有关内敛函数的一些特性
2、特性①:空间换时间
有同学可能不是很理解空间换时间是什么意思,可以看看时空复杂度章节。不过要说明的一点是==这里的空间不是内存,是编译后的程序,而【空间换时间】就会使得编译出来的可执行程序会变大==
- 对于这种空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用
- 【缺陷】:可能会使目标文件变大;
- 【优势】:少了调用开销,提高程序运行效率;
- 可以通过下面这幅图来看看:此时程序中有一个
swap()函数,内部有10行代码。在程序的1000个地方都会对其进行调用,那请问此时去使用内联函数和不使用内联函数的区别是什么?
- 当不使用内联函数时,就是将这个
swap()函数当做普通的函数的话,我们知道在底层会进行一个call指令再通过jump指令跳转到这个函数所在地址然后执行这个函数,那每一个调用的地方都要跳转的话,就会跳转1000次,==所以swap + 调用swap指令,合计是1000 + 10条指令== - 当使用内联函数时,通过上面的学习可以知道,它不会去进行一个函数的调用,而是直接将相关的指令拷贝到程序中调用这个
swap()函数的每一块地方,==所以swap + 调用swap指令,合计是1000 * 10条指令==
- 可以看出,在使用内联函数减少函数调用的同时也会增加程序的负担,使得目标文件会变得很大
🎁趣味杂谈:庞大的游戏更新包
在理解了一个函数设不设置内联函数的区别的时候,顺带来谈一谈这个目标文件体积变大的缘故
- 要知道,干我们这一行基本一年到头都是在公司,也有逢年过节才能回家一趟,而且平常工作忙得也没有时间玩游戏,那此刻当你过年回家的时候,就看到家里的一些弟弟妹妹拿着手机📱在打《王者荣耀》,这相信是最正常不过了
- 那此时呢,你也心里痒痒,打开了你尘封已久的王者荣耀,然后看到要更新,而且一看就是4、5个G,突然之间就不想玩了😅更新的过程中只能眼巴巴地看着别人玩,
- 不过呢,你有个同事,也是很久都没有玩过游戏了,也是一样进去更新,但是呢她却3、5分钟更新好了,因为更新包只有3、500M。这是为什么呢?原来她是苹果手机,系统是IOS系统,其实对于更新的这些内容都是
指令,只是你们的指令数目不同而已。那我们都知道【苹果】这家公司系统是自己设计的,而且对于像芯片、硬件、软件各方面都领先于同行 - 所以说这个性能不一样就会手机系统上使用的软件也存在差异。别人这个性能好,在优化各种方面都齐全,所以就不会导致这个更新包越变越大,相反对比我们平常使用的几千元的安卓手机,却越用越卡,这也是很多人选择使用苹果手机的原因👈
3、特性②:inline实现机制
好,小插曲,我们回归正题,继续来讲讲内联函数的第二大特性 —— 【inline实现机制】
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同
- 一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性
《C++prime》第五版关于inline的建议:
我们可以到VS中来观察一下
- 可以观察到当内联函数的内容增多时,程序中去调用这个内联函数依旧会执行
call指令,而不是将其函数中的内容直接拷贝到程序调用处,即使是加了这个inline关键字,似乎还是起不到内联函数的作用,这是为什么呢? - 因为编译器在对于这个内联请求的时候发觉不对劲,所以选择忽略了这个请求,而不是像宏那样无论怎样都会傻傻地进行替换
- 这其实就是内联函数在替代宏之后很优秀的一个特性,假设说现在你这个设置的内联函数有1000多行代码,在一个大项目中又有1000个地方调用了这个内联函数。
- 如果不采用将其展开去调用的话消耗的顶多也就是
1000 + 1000条指令 - 如果采用内联将其展开的话消耗的就是
1000 * 1000条指令,这就很恐怖了😱
- 那上面这个还是个普通的大一点的函数,但你再想如果是**··**呢?层层地往下调用再一层层地返回来,那需要调用的指令就更多了,如果全部站开的话,就会造成一个灾难性的后果⚠
所以呢,加不加内敛是你的事,最终要不要把它真的展开变成内敛编译器说了算,所以我们在使用的时候一般是比较短小、调用频繁的函数(10几行以内)加上内联,其他就不加了
4、特性③:inline的声明与定义
inline内联函数的第三个特性,就是我们要注意内联函数的定义和声明不可以分开,导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
// F.h #include <iostream> 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; }
- 通过去VS中执行这段分文件代码,可以发现它不是一个【编译时错误】,而是一个【运行时错误】
👉 这种错误一般都是最后链接目标文件的时候除了问题,不是很清楚的可以看看程序的编译和链接👉 对于这些很复杂的符号func@@YAXH@Z,是C++中的函数名修饰,可以看看探究函数重载的原理:函数名修饰
那我们现在就要去思考,为什么会出现这种【运行时错误】呢?
- 最主要的一点首先你要知道:==内联函数的不进符号表的==,因为对于内联函数来说它在其调用的地方都展开了,所以不需要产生一串指令把它放到符号表中,再通过一条一条指令去调用
反汇编观察函数调用的流程
对于内联函数来说是这样,但是普通函数来说是怎么一步步地进行调用的呢?我们可以通过汇编来看看:computer:
- 对于函数的地址来说,在指针章节我们有说过,它值得就是【函数指针】,但是从汇编角度来看,它
call的到底是谁的地址呢?
- 通过按下
F11跳转到了一条叫做【jmp】的指令,此时你去观察这个地址,其实就是这条【jmp】的地址,在英语中jump是跳的意思,那你可以将这条指令理解为一个中转站,call指令会暂时跳到这条指令所在的地址,然后再通过这条指令去找需要调用函数的地址
- 此刻,再按下
F10的话就会通过这个函数的地址找到这个函数,此时就开始执行函数内部的逻辑,
- 然后,当执行完函数内部的逻辑后,便回到了
call指令的下一条指令,这就算是一次函数调用的过程
==可以将它们放到一起来观察,就可以发现它们之间的逻辑非常紧密==
如果对上面过程还是不太理解的可以看看反汇编深挖【函数栈帧】的创建和销毁
内联函数的生成机制【⭐】
上面是对于普通函数的调用机制,那对于内联函数呢?也是这么去跳转吗?
- 一定要注意!内联内联,那么这个函数的内容就直接放到调用的地方
- 它便说:也就是我不需要你再通过这么繁琐的步骤一步步地跳转过来了,我会将内部的东西做一些优化,直接放到你那里,你执行这些指令即可
可是呢,为什么会出现链接错误❌
- 因为在【预编译】的时候就要展开
func.h这个头文件,但是在主调用接口中包含的头文件中只有函数的声明没有实现,此时只能在【链接】的时候展开了,但是在链接的时候因为只有声明所以只得到了函数名修饰后的地址。编译器便需要通过这个地址找到函数所在的位置,对于普通函数而言在这个时候就可以通过call找过去了,但是对于==内联函数==而言,却无法做到,因为它并没有【call】和【jmp】这些指令,因此就造成了链接错误的现象
那要如何去解决呢?
- 若是这个函数要定义成内联函数的话,就不要将定义和声明分开了,在头文件中定义出来后就直接对其进行声明,便不会造成这样的问题了
// F.h #include <iostream> using namespace std; inline void f(int i) { cout << i << endl; } //-------------------------- // main.cpp #include "F.h" int main() { f(10); return 0; }
✍一道经典笔试题
关于c++的inline关键字,以下说法正确的是( )
A.使用inline关键字的函数会被编译器在调用处展开 B.头文件中可以包含inline函数的声明 C.可以在同一个项目的不同源文件内定义函数名相同但实现不同的inline函数 D.递归函数也都可以成为inline函数
【答案】:C
【解析】:
- [A] 不一定,因为inline只是一种建议,需要看此函数是否能够成为内联函数
- [B] inline函数不支持声明和定义分离开,因为编译器一旦将一个函数作为内联函数处理,就会在调用位置展开,即该函数是没有地址的,也不能在其他源文件中调用,故一般都是直接在源文件中定义内联函数的
- [C] inline函数会在调用的地方展开,所以符号表中不会有inline函数的符号名,不存在链接冲突
- [D] 比较长的函数,递归函数就算定义为inline,也会被编译器忽略,故错误
四、总结与提炼
好,最后来总结一下本文所学习的内容:book:
- 首先我们回顾来一下C语言中所学习的【宏】,经过了对宏的优缺点分析以及一些同学的错误案例对照,在面试的时候让你写一个宏,可不要写错了哦!
- 接下去,我们就正式地开始介绍内联函数,对于内联函数来说,它不仅保留了宏的优点,没有函数调用建立栈帧的开销,而且还修复了宏的缺点,将其做成一个函数的形式,简洁直观,而且便于调试观察,提升程序运行的效率
- 但是对于内联函数来说,也是存在要注意的地方,因为它也是会和宏一样在调用的地方展开,不过会进行一定程度的优化,可这种空间换时间的思想只适用于小型的函数,对于大型的函数不建议定义成【内联函数】,会造成程序的过多臃肿
- 但是在看了内联函数的生成机制后,其实我们也不用担心在误用内联函数后使得程序变大,它会有一个自动判断的机制,若是你程序的行数过多的话,编译器就会忽略你的这请求,对于我们要将一个函数声明为内联函数其实是在向编译器发起一个申请,它可以选择接收也可以选择拒绝🙅
以上就是本文要介绍的所有内容,感谢您的阅读:rose:
















