C++ | 对比inline内联函数和宏的不同点-2

简介: C++ | 对比inline内联函数和宏的不同点

三、inline内联函数

接下去我们就正式来讲讲C++中的独有的【内联函数】

1、概念

inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率

  • C++的祖师爷呢,认为【宏】存在一定的缺陷,但是呢也有它的好处,也就是直接做替换,不需要开辟函数栈帧,所以在C++中就出现了内敛函数这么一个东西
inline int Add(int x, int y)
{
  int z = x + y;
  return z;
}
  • 但是它真不会去进行函数调用吗?我们到VS中来看看

image.png

  • 那此时就又同学疑惑了🤨这不还是会有call指令吗,哪来的不调用一说呢?
  • 因为我们还需要去做一些配置💻

image.pngimage.pngimage.png

  • 然后我们再去观察一下汇编就可以发现不存在call指令了,编译器直接将内敛函数Add做了展开

image.png

  • 所以C++中我们是不推荐用宏的,因为有内联函数这个特性,即保留了宏的优点,无需调用函数建立栈帧,而且还修复了宏的缺陷,不再需要将内容写得那么复杂,写成日常的函数形式即可,只需要在前面加上一个inline关键字,就可以起到这种效果。非但如此,它还可以调试:computer:

接下去我们再来介绍一下有关内敛函数的一些特性

2、特性①:空间换时间

有同学可能不是很理解空间换时间是什么意思,可以看看时空复杂度章节。不过要说明的一点是==这里的空间不是内存,是编译后的程序,而【空间换时间】就会使得编译出来的可执行程序会变大==

  • 对于这种空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用
  • 【缺陷】:可能会使目标文件变大;
  • 【优势】:少了调用开销,提高程序运行效率;
  • 可以通过下面这幅图来看看:此时程序中有一个swap()函数,内部有10行代码。在程序的1000个地方都会对其进行调用,那请问此时去使用内联函数和不使用内联函数的区别是什么?
  • 当不使用内联函数时,就是将这个swap()函数当做普通的函数的话,我们知道在底层会进行一个call指令再通过jump指令跳转到这个函数所在地址然后执行这个函数,那每一个调用的地方都要跳转的话,就会跳转1000次,==所以swap + 调用swap指令,合计是1000 + 10条指令==
  • 当使用内联函数时,通过上面的学习可以知道,它不会去进行一个函数的调用,而是直接将相关的指令拷贝到程序中调用这个swap()函数的每一块地方,==所以swap + 调用swap指令,合计是1000 * 10条指令==

image.png

  • 可以看出,在使用内联函数减少函数调用的同时也会增加程序的负担,使得目标文件会变得很大

🎁趣味杂谈:庞大的游戏更新包

在理解了一个函数设不设置内联函数的区别的时候,顺带来谈一谈这个目标文件体积变大的缘故

  • 要知道,干我们这一行基本一年到头都是在公司,也有逢年过节才能回家一趟,而且平常工作忙得也没有时间玩游戏,那此刻当你过年回家的时候,就看到家里的一些弟弟妹妹拿着手机📱在打《王者荣耀》,这相信是最正常不过了
  • 那此时呢,你也心里痒痒,打开了你尘封已久的王者荣耀,然后看到要更新,而且一看就是4、5个G,突然之间就不想玩了😅更新的过程中只能眼巴巴地看着别人玩,
  • 不过呢,你有个同事,也是很久都没有玩过游戏了,也是一样进去更新,但是呢她却3、5分钟更新好了,因为更新包只有3、500M。这是为什么呢?原来她是苹果手机,系统是IOS系统,其实对于更新的这些内容都是指令,只是你们的指令数目不同而已。那我们都知道【苹果】这家公司系统是自己设计的,而且对于像芯片、硬件、软件各方面都领先于同行
  • 所以说这个性能不一样就会手机系统上使用的软件也存在差异。别人这个性能好,在优化各种方面都齐全,所以就不会导致这个更新包越变越大,相反对比我们平常使用的几千元的安卓手机,却越用越卡,这也是很多人选择使用苹果手机的原因👈

image.png

3、特性②:inline实现机制

好,小插曲,我们回归正题,继续来讲讲内联函数的第二大特性 —— 【inline实现机制】

  • inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同
  • 一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性

《C++prime》第五版关于inline的建议:

image.png我们可以到VS中来观察一下

  • 可以观察到当内联函数的内容增多时,程序中去调用这个内联函数依旧会执行call指令,而不是将其函数中的内容直接拷贝到程序调用处,即使是加了这个inline关键字,似乎还是起不到内联函数的作用,这是为什么呢?
  • 因为编译器在对于这个内联请求的时候发觉不对劲,所以选择忽略了这个请求,而不是像宏那样无论怎样都会傻傻地进行替换

image.png

  • 这其实就是内联函数在替代宏之后很优秀的一个特性,假设说现在你这个设置的内联函数有1000多行代码,在一个大项目中又有1000个地方调用了这个内联函数。
  • 如果不采用将其展开去调用的话消耗的顶多也就是1000 + 1000条指令
  • 如果采用内联将其展开的话消耗的就是1000 * 1000条指令,这就很恐怖了😱
  • 那上面这个还是个普通的大一点的函数,但你再想如果是**··**呢?层层地往下调用再一层层地返回来,那需要调用的指令就更多了,如果全部站开的话,就会造成一个灾难性的后果⚠

image.png

所以呢,加不加内敛是你的事,最终要不要把它真的展开变成内敛编译器说了算,所以我们在使用的时候一般是比较短小、调用频繁的函数(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中执行这段分文件代码,可以发现它不是一个【编译时错误】,而是一个【运行时错误

image.png👉 这种错误一般都是最后链接目标文件的时候除了问题,不是很清楚的可以看看程序的编译和链接👉 对于这些很复杂的符号func@@YAXH@Z,是C++中的函数名修饰,可以看看探究函数重载的原理:函数名修饰

image.png那我们现在就要去思考,为什么会出现这种【运行时错误】呢?

  • 最主要的一点首先你要知道:==内联函数的不进符号表的==,因为对于内联函数来说它在其调用的地方都展开了,所以不需要产生一串指令把它放到符号表中,再通过一条一条指令去调用

反汇编观察函数调用的流程

对于内联函数来说是这样,但是普通函数来说是怎么一步步地进行调用的呢?我们可以通过汇编来看看:computer:

  • 对于函数的地址来说,在指针章节我们有说过,它值得就是【函数指针】,但是从汇编角度来看,它call的到底是谁的地址呢?

image.png

  • 通过按下F11跳转到了一条叫做【jmp】的指令,此时你去观察这个地址,其实就是这条【jmp】的地址,在英语中jump是跳的意思,那你可以将这条指令理解为一个中转站,call指令会暂时跳到这条指令所在的地址,然后再通过这条指令去找需要调用函数的地址

image.png

  • 此刻,再按下F10的话就会通过这个函数的地址找到这个函数,此时就开始执行函数内部的逻辑,

image.png

  • 然后,当执行完函数内部的逻辑后,便回到了call指令的下一条指令,这就算是一次函数调用的过程

image.png

==可以将它们放到一起来观察,就可以发现它们之间的逻辑非常紧密==

image.png如果对上面过程还是不太理解的可以看看反汇编深挖【函数栈帧】的创建和销毁

内联函数的生成机制【⭐】

上面是对于普通函数的调用机制,那对于内联函数呢?也是这么去跳转吗?

  • 一定要注意!内联内联,那么这个函数的内容就直接放到调用的地方
  • 它便说:也就是我不需要你再通过这么繁琐的步骤一步步地跳转过来了,我会将内部的东西做一些优化,直接放到你那里,你执行这些指令即可

可是呢,为什么会出现链接错误❌

  • 因为在【预编译】的时候就要展开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:

相关文章
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
278 1
|
C语言 C++
C++(三)内联函数
本文介绍了C++中的内联函数概念及其与宏函数的区别。通过对比宏函数和普通函数,展示了内联函数在提高程序执行效率方面的优势。同时,详细解释了如何在C++中声明内联函数以及其适用场景,并给出了示例代码。内联函数能够减少函数调用开销,但在使用时需谨慎评估其对代码体积的影响。
|
安全 编译器 C++
C++入门 | 函数重载、引用、内联函数
C++入门 | 函数重载、引用、内联函数
158 5
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
201 0
|
程序员 C++ 开发者
C++入门教程:掌握函数重载、引用与内联函数的概念
通过上述介绍和实例,我们可以看到,函数重载提供了多态性;引用提高了函数调用的效率和便捷性;内联函数则在保证代码清晰的同时,提高了程序的运行效率。掌握这些概念,对于初学者来说是非常重要的,它们是提升C++编程技能的基石。
172 0
|
存储 安全 编译器
【C++入门 四】学习C++内联函数 | auto关键字 | 基于范围的for循环(C++11) | 指针空值nullptr(C++11)
【C++入门 四】学习C++内联函数 | auto关键字 | 基于范围的for循环(C++11) | 指针空值nullptr(C++11)
|
算法 编译器 C++
C++基础知识(三:哑元和内联函数和函数重载)
在C++编程中,"哑元"这个术语虽然不常用,但可以理解为在函数定义或调用中使用的没有实际功能、仅作为占位符的参数。这种做法多见于模板编程或者为了匹配函数签名等场景。例如,在实现某些通用算法时,可能需要一个特定数量的参数来满足编译器要求,即使在特定情况下某些参数并不参与计算,这些参数就可以被视为哑元。
563 1
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
12月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
459 12
|
10月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
249 0