2022-05-17-
摘要
总结
目录
内联函数
C++中函数的使用我们已经比较清楚了,与C语言中函数的使用大多相同,主要是增加了重载的特性,对C语言的函数的一些缺陷做了一些补充。
那么对于一些比较简单却又经常使用的功能,我们在C语言中常常使用宏来替换,宏呢与函数相比没有栈帧的辟,类型的检查,没有传参,仅仅是做一个替换,非常适合功能简单却使用频繁的应用场景,但是宏正因为如此,也就具有了不安全、无法调试的缺陷,那么C++中如何处理这样地缺陷呢?
内联函数应运而生它既继承了宏的优点也继承了函数的优点,即既没有开辟栈帧的开销,又可以去调试,并且有类型的检查。
内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以像调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。
概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,
内联函数提升程序运行的效率。
函数是一个可以重复使用的代码块,CPU 会一条一条地挨着执行其中的代码。CPU 在执行主调函数代码时如果遇到了被调函数,主调函数就会暂停,CPU 转而执行被调函数的代码;被调函数执行完毕后再返回到主调函数,主调函数根据刚才的状态继续往下执行。
我们得明白,函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。
如果函数体代码比较多,需要较长的执行时间,那么函数调用机制占用的时间可以忽略;如果函数只有一两条语句,那么大部分的时间都会花费在函数调用机制上,这种时间开销就就不容忽视,要尽可能处理函数调用机制所用时间占比大的这种情况。
为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数(Inline Function),又称内嵌函数或者内置函数。
先来看看普通函数的调用过程:
调用函数时是使用call指令,去调用某地址上的函数。(注意:函数都是有地址的,可以用以区分内联函数)
如果在上述函数的前面加上inline关键字将其改为内联函数,在编译期间编译器会用函数体替换函数的调用。
不过我们通常在Debug模式下默认函数不会被当做内联,即使你加上了inline,都会被编译器忽略,只有在release模式下,inline才有可能会被采纳,至于为什么是有可能,编译器只会把你的inline关键字当做一个建议,至于编译器是否按照你所要求的去做,这就不一定了,因为这仅仅是一个建议,编译器会结合具体情况比如函数体指令的多少来判断到底是否当做内联函数。
所以我们如何去观察一个函数是否被当做内联函数呢?
在release模式下
-
查看编译生成的汇编代码中是否存在call Add
-
监视器窗口查看Add函数是否有地址;
在debug模式下需要对编译器进行设置,否则不会展开,因为在debug模式下,编译器默认不会对代码进行优化,内联函数其实算一种优化方式。
- 在项目—>属性中找到 C/C++选项—>常规
将调试信息格式改为程序数据库(/Zi)
-
在C/C++选项中找到优化
将内联函数扩展选择—>只适用于__inline(Ob1)
- 重新生成可执行文件即可
完成后,我们便可以在debug模式下查看到内联函数的展开
这里并没有call Add函数,而是函数体的展开(当然不仅仅是简单的展开,还会涉及一些其他指令,不做深入讨论)。
特性
- inline是一种空间换时间的做法 ,节省了开辟栈帧的时间开销;
与调用普通函数相比不需要去开辟栈帧空间,节省了时间,相当于inline函数体所有指令都在当前栈内被执行;
- inline对于编译器仅仅是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等,编译器优化会自动忽略内联。
不仅是以上两种情况,函数体内的指令一旦较多,编译器就会自动忽略,如下:
函数体指令较复杂:
函数体指令较简单:
- inline函数不建议声明和定义分离,分离会导致链接错误。因为inlinn函数被展开,也就不会有函数地址,自然不用提去链接了。
//func.h文件
#pragma once
#include<iostream>
using namespace std;
inline void f(int i);
//func.cpp文件
#include"test.h"
void f(int i)
{
cout << "func" << endl;
}
//main.cpp文件
#include"test.h"
int main()
{
f(1);
return 0;
}
报错:error LNK2019: 无法解析的外部符号 “void __cdecl f(int)” (?f@@YAXH@Z),函数 main 中引用了该符号。
对于内联函数,其工作原理是:
对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。
各个文件是分离编译的,在func.c中由于声明了f函数是内联的,并且函数体也很简短,因此编译器遵循了我们的建议,使其成为一个内联函数,由于没有函数地址,自然无法被除本源文件以外的地方调用;也可以说内联函数在符号表不会有合并这一步操作,仅仅存在于本源文件中。
内联函数的缺点
难道内联函数就没有缺点吗,当然有!不然还要函数做什么?内联函数随着一次次的调用展开,会造成代码膨胀的问题,通俗讲就是生成的可执行文件会变大,这是我们不愿意看到的(有谁愿意看着自己的电脑硬盘被榨干呢?)
可以大致从几个方面看:
- 编译后的程序会存在多份相同的函数拷贝,这些函数拷贝都将占据内存,如果被声明为内联函数的函数体非常大,那么编译后的程序体积也将会变得很大。
很好理解,普通的函数都有一个地址,每当我们需要使用这个函数时,直接通过函数名访问地址,然后就是 建 立栈帧的过程,在新栈帧中执行相应函数指令。
举一个例子就是,普通函数就是一个坚信好 “记性不如烂笔头的乖学生”,老师讲一个重要的、多次使用的知识点时,他就记在笔记本上,需要了就拿出来看看就会了。内联函数也是一个爱记笔记的学生,不过它丢三落四的,刚记下笔记笔记本就丢了,每次需要时,就只能又去问老师再记下来,慢慢的他写过的笔记本就很多了,不过他自己还浑然不知。
他们两个同学的笔记本都是一个作用,就是记录下这个知识