【C++】内联函数深度解析(什么是内联函数?为什么要使用内联函数?)

目录

一、前言

二、什么是内联函数? 

三 、为什么要使用内联函数?

四、分析【宏】的优缺点

1、概念回顾 

2、宏的缺点 

3、宏的优点 

五、inline内联函数深度解析

1、概念 

 2、内联函数的特性

特性①:空间换时间 

特性②:inline实现机制 

特性③:inline的声明与定义 

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

✍内联函数经典笔试题  

六、总结与提炼

七、共勉 


一、前言

       本文介绍了C++内联函数的概念,其如何在编译时展开以提高效率,以及与宏函数的区别。内联函数适用于短小频繁调用的情况,但并非所有函数都适合。还讨论了内联函数的优缺点和是否一定能被编译器展开的问题。

二、什么是内联函数? 

1.直观上定义:

【内联函数】的定义 与 【普通函数】 基本相同,只是在函数定义前加上关键字 inline

// 声明一个内联函数
inline int add(int a, int b) 
{
    return a + b;
}

2.更深入的思考: 

  • 函数前面加上inline一定会有效果吗?
  • 如果不加inline就不是内联函数了吗?
  • 后面让我们慢慢来解答这两个问题 

三 、为什么要使用内联函数?

  • 内联函数最初的目的:代替部分 #define 宏定义
  • 使用内联函数替代普通函数的目的:提高程序的运行效率;

什么是【宏】? 

那么为什么 要代替 #define ; 

为何能提高运行效率呢?我们下面一次进行解析 

四、分析【宏】的优缺点

在将 内联函数 之间前,我们再来聊聊有关的一些内容 

1、概念回顾 

下面是【宏】的申明方式: 

#define name( 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));
  • 可是呢,有的同学虽然想到了这一点,但是却在最后面加了一个;】号,对于分号来说是我们在写代码的时候作为一条语句结束的标志,但是对于宏来说也可以写分号吗?
  • 将原先的传参继续替换成下面这样来试试

  • 最后这一种才是最正确的写法
#define ADD(x, y) ((x) + (y))		//✔

2、宏的缺点 

看了上面有关【宏】概念的一些回顾,我们来聊聊它的缺点所在 

可能会带来运算符优先级的问题,导致程容易出现错。【加括号太麻烦了!!!】 

  • 这一点相信你在看了上一小节的叙述之后一定是感同身受,只是完成一个两数相加的功能,就需要加上这么多括号了,若是再复杂一些的功能,那岂不是要加很多了🙄

 宏是不能调试的【这点比较致命👈】

Linux环境下gdb 

没有类型安全的检查【直接替换】 

  • 可以看到,无论我传入何种类型的参数,都不会出现问题。这点其实说明了宏对于类型的检查是不严谨的

3、宏的优点 

了解了宏的缺点之后,我们再来瞧瞧它的优点,既然在一些场景被广泛使用,那一定也具有它的优点💡 

① 宏常量提高复用性和可维护性 

  • 这一点很好理解,也就是我们平常用得最多的,一般会将频繁使用到的一些数字定义成一个宏,这样就不需要每次写数字了,直接写这个宏所定义的变量即可
  • 而且在修改的时候也会很方便,只需要修改一下宏定义处即可,实现一改多改【这一点在项目里面还是比较常用的
#define n 500

② 宏函数类型不固定,int、double、float都可以用

  • 这一点其实我们在讲缺点的时候也有提及,虽然类型检查得不严谨,但真正用起来还是很香的,这很像是我们之前说到过的函数重载,虽然函数名相同,但是可以传入不同的参数实现不同的运算

③ 宏函数不用建立栈帧,因此不会产生消耗 

  • 刚才我们有观察过,对于【宏】来说是不能调试的,因为它根本就不是一个真正意义上的函数,只是具备函数的功能罢了。既然不是函数的话也就不会在栈上建立函数栈帧,那也就不会有过多的消耗

好,以上就是本文所要讲的有关【宏】相关的所有内容,希望能唤起读者对这个知识点的回忆 

五、inline内联函数深度解析

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

1、概念 

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

  • C++的祖师爷呢,认为【宏】存在一定的缺陷,但是呢也有它的好处,也就是直接做替换,不需要开辟函数栈帧,所以在C++中就出现了【内联函数】来代替【

示意代码对比 

假设有以下两个版本的代码,一个是普通函数调用,另一个是内联函数调用: 

// 普通函数版本
int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 10;
    int y = 20;

    // 普通函数调用
    int result = add(x, y);

    return 0;
}
// 内联函数版本
inline int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 10;
    int y = 20;

    // 内联函数调用
    int result = add(x, y);

    return 0;
}
  • 编译器可能会将这两段代码生成以下汇编代码(为了简化理解,以下汇编代码是伪代码,仅用于展示内联化的效果):

  • 普通函数:编译器生成的代码包含了函数调用(call add),即需要额外的步骤来保存状态、传递参数、执行函数体,然后再返回主程序。
  • 内联函数:编译器直接将 add 函数的代码替换到了调用处,因此没有函数调用的开销。相当于编译器在 main 函数中直接插入了 add 函数的代码,从而减少了额外的跳转和栈操作

通过这种方式,内联函数可以优化性能,特别是在频繁调用的小型函数中,减少不必要的开销。

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

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

 2、内联函数的特性

特性①:空间换时间 

  • 有同学可能不是很理解空间换时间是什么意思,这里的空间不是内存,是编译后的程序,而【空间换时间】就会使得编译出来的可执行程序会变大
  • 对于这种空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用
    • 【缺陷】:可能会使目标文件变大;
    • 【优势】:少了调用开销,提高程序运行效率;

可以通过下面这幅图来看看:此时程序中有一个swap()函数,内部有 10 行代码。在程序的1000个地方都会对其进行调用,那请问此时去使用内联函数和不使用内联函数的区别是什么?

  • 当不使用内联函数时,就是将这个swap()函数当做普通的函数的话,我们知道在底层会进行一个call指令再通过jump指令跳转到这个函数所在地址然后执行这个函数,那每一个调用的地方都要跳转的话,就会跳转1000次,所以swap + 调用swap指令,合计是1000 + 10条指令
  • 使用内联函数时,通过上面的学习可以知道,它不会去进行一个函数的调用,而是直接将相关的指令拷贝到程序中调用这个swap()函数的每一块地方,所以swap + 调用swap指令,合计是1000 * 10条指令

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


特性②:inline实现机制 

我们继续来讲讲内联函数的第二大特性 —— 【inline实现机制】 

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

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

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

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

这其实就是内联函数在替代宏之后很优秀的一个特性,假设说现在你这个设置的内联函数有1000多行代码,在一个大项目中又有1000个地方调用了这个内联函数。 

  • 如果不采用将其展开去调用的话消耗的顶多也就是1000 + 1000条指令
  • 如果采用内联将其展开的话消耗的就是1000 * 1000条指令,这就很恐怖了😱
  • 那上面这个还是个普通的大一点的函数,但你再想如果是**··**呢?层层地往下调用再一层层地返回来,那需要调用的指令就更多了,如果全部站开的话,就会造成一个灾难性的后果⚠

 所以呢,加不加内联是你的事,最终要不要把它真的展开变成内联编译器说了算,所以我们在使用的时候一般是比较短小、调用频繁的函数(10几行以内)加上内联,其他就不加了


特性③: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.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,也会被编译器忽略,故错误

函数前面加上inline一定会有效果吗? 

答:不会,使用内联inline关键字修饰函数只是一种提示,编译器不一定认。 


如果不加inline就不是内联函数了吗? 

答:存在隐式内联,不用inline关键字,C++中在类内定义的所有函数都自动称为内联函数。 


总结: 

内联函数使用于短小而频繁调用的函数。如果函数比较长、又或者是递归函数不要让其称为内联函数。此外注意内联函数应省略原型,将函数头和所有函数代码放在本应提供原型的地方 


六、总结与提炼

好,最后来总结一下本文所学习的内容📖 

  • 首先我们回顾来一下C++语言中所学习的【宏】,经过了对宏的优缺点分析以及一些同学的错误案例对照,在面试的时候让你写一个宏,可不要写错了哦!
  • 接下去,我们就正式地开始介绍内联函数,对于内联函数来说,它不仅保留了宏的优点,没有函数调用建立栈帧的开销,而且还修复了宏的缺点,将其做成一个函数的形式,简洁直观,而且便于调试观察,提升程序运行的效率
  • 但是对于内联函数来说,也是存在要注意的地方,因为它也是会和宏一样在调用的地方展开,不过会进行一定程度的优化,可这种空间换时间的思想只适用于小型的函数,对于大型的函数不建议定义成【内联函数】,会造成程序的过多臃肿
  • 但是在看了内联函数的生成机制后,其实我们也不用担心在误用内联函数后使得程序变大,它会有一个自动判断的机制,若是你程序的行数过多的话,编译器就会忽略你的这请求,对于我们要将一个函数声明为内联函数其实是在向编译器发起一个申请,它可以选择接收也可以选择拒绝🙅‍

七、共勉 

以下就是我对 【C++】内联函数 的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对 【C++11】 的理解,请持续关注我哦!!!        

  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值