以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
在C++语言中,宏函数的使用是很便利的,但是它也存在着他的缺陷: 1、宏函数可能会存在一定副作用 2、在预处理阶段进行替换,不会参与编译,少了类型检测 3、宏函数不能调试 (等等其他缺陷,这里只提出三个主要缺陷) 具体表现在哪呢,我们可以通过下面一个代码和运行结果来看:
#include <iostream> using namespace std; #define MAX(a,b) (a>b?a:b) //一个输出两者较大值的宏函数 int main() { int a = 20; int b = 10; cout << MAX(++a, b) << endl;//在比较时,a已经自加一次,变成21,返回的时候又自加一次变成22了; system("pause"); return 0; }这个代码的输出结果为: 按道理来讲,我们用++a和b进行比较,输出最大值,应该是21和10的比较,输出最大值,即21。但是使用宏函数的方法,就会先自加一次,去参与比较,变成21,返回的时候再去自加一次,进行返回,然后输出22。 这跟我们的预期是有区别的,这就是上文提到的,宏函数的缺陷之一,就是会存在一定的副作用。 当然,宏函数也不一定全有缺陷,存在缺陷的只是部分,就比如我们提到的这个宏函数。 宏函数的缺陷之二就是我们提到的:在预处理阶段进行替换,不会参与编译,少了类型检测。 同样的代码,我们修改一下看看:
#include <iostream> using namespace std; #define MAX(a,b) (a>b?a:b) //一个输出两者较大值的宏函数 int main() { int a = 20; int b = 10; cout << MAX("hello",100) << endl; system("pause"); return 0; }在上面这个代码中,我们可以看到宏函数中,变量a和b是没有定义类型的,因此我们去编译一下就会发现:
可以看出,这个代码在预处理阶段是不能通过的,但是报错是出在打印的时候,而不是宏函数定义的时候,这对于代码量相对复杂的情况下,是很难找到具体是什么问题的,因为一般情况下,我们通常都不会觉得宏函数和宏常量那里会出问题。
这也是宏函数的缺陷之二,就是它不会参与编译,少了类型检测,就会出现类型不兼容的问题。
宏函数的缺陷之三就是无法进行调试。
这个缺陷应该怎么去理解呢?就是宏函数不像其他函数,它是在预处理阶段就开展的,所以没办法进行调试。
基于宏函数存在的种种缺陷,C++中引入内联函数解决此类问题,它继承了宏函数的优点,又规避了宏函数的缺陷。
注意:宏函数的优点主要指相对于函数的优点。即宏函数在预处理阶段就展开了,所以少了函数调用的开销(即传参、参数压栈以及栈帧开销)
我们先来看一下一个普通函数:
int Add(int left, int right) { return left + right; } int main() { int a = 10; int b = 20; int c = 0; c = Add(a, b); cout << c << endl; return 0; }我们来看一下它的反汇编:
从反汇编中可以看到这个“call”指令,并且调用到这个Add函数了,也就说明并没有发生替换,只有内联函数才会发生替换。 我们加上“inline”,将其修饰为内联函数,再来看一下它的反汇编:
inline int Add(int left, int right) { return left + right; } int main() { int a = 10; int b = 20; int c = 0; c = Add(a, b); cout << c << endl; return 0; }可以看到,加了“inline”修饰的内联函数的反汇编依旧有“call”指令,并且调用了Add函数,这是什么原因呢?
这是因为刚才的代码以及反汇编,都是在debug模式下进行的,但是在debug模式下,内联函数默认是不会展开的,因为debug模式主要是用来调试代码的,如果编译器将内联函数展开了,代码就不方便调试了。
我们在release模式看一下这个代码的反汇编: 首先修改了一下原先的代码:
inline int Add(int left, int right) { return left + right; } int main() { int a = 10; int b = 20; int c = 0; c = Add(a, b); printf("%d\n", c); //cout << c << endl; return 0; }我们将输出变量c的方式改为”printf“的方式,”printf“是C语言中的一个函数,这里输出c,就相当于直接调用“printf”函数。 我们来看一下它的反汇编: 这里依旧有call指令,但是明显的是,call指令后面调用的已经不是Add这个函数了,而是printf这个函数,而上面的1EH就是a+b的结果。就相当于main函数中的5行代码,被简化成了printf(“%d\n”,30)。我们可以很明显的看出,Release模式下,编译器对代码的优化其实是相当明显的。
我们可以来做一个比较,在创建的工程里面看一下,debug模式和Release模式下的.exe文件的大小。 从上面两个图的比较当中,我们可以发现debug模式下的“.exe”文件是远大于Release模式下的,即我们可以很明显的看出,Release模式下,编译器对代码的优化其实是很明显的。
我们刚才提到过在Debug模式下,内联函数默认是不会展开的。想要展开内联函数,我们需要进行编译器的设置。具体设置方式如下(以VS2013为例):
首先是程序数据库的设置: 其次是内联函数扩展的设置: 设置完成后,再在Debug模式下,看一下这个代码的反汇编: 从这个图里面,我们可以发现,已经没有“call”指令调用Add函数了,整个代码相当于是将a放进eax,b放进eax,然后对其相加,即代表着内联函数被展开了。这个代码的运行效率就得到了很大的提高,可以直接对其相加,不用再去调用这个函数,从而进行开辟栈帧等一系列操作,这个代码的运行效率就得到了很大的提高。
1、inline是一种以空间换时间的做法,省去调用函数的开销,因此在代码很长的或者有循环/递归的函数不适宜使用作为内联函数。
举个例子:
int main() { int a = 10; int b = 20; int c = 0; Add(a, b); Add(a, b); Add(a, b); Add(a, b); Add(a, b); printf("%d\n", c); //cout << c << endl; return 0; }假如Add是一个五行的函数,全部放在main函数里,一个个调用函数,就会需要开辟很多栈帧,但是使用内联函数的方式,对于这种比较简短的函数,就会直接对其返回,不用一次次的调用,只需要在main函数里面直接展开即可。 但是对于比较复杂的函数,即使使用了inline的关键字,编译器也不一定会处理(注意:是不一定而不是一定不)。
2、inline对编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等,编译器优化时会忽略掉内联。
意味着编译器有自己优化的选择权,如果编译器认为作为内联函数更好,就会将其展开,如果觉得不好,就不会进行展开,即使使用了inline的关键字。
3、inline不建议声明和定义分离,分离会导致连接==链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
这里我们分文件去处理一下,尝试是否可以使用内联函数: 首先添加一个Add.h文件
#pragma once; int Add(int left, int right);再添加一个Add.cpp文件
#include "Add.h" int Add(int left, int right) { return left + right; }再在源文件主函数里面去调用它
#include "Add.h" int main() { Add(10, 20); return 0; }我们对他进行编译,是通过的,我们可以查看一下它的反汇编: 因为我们在定义函数的时候没有加inline关键字,因此它的反汇编中,就会有call指令调用Add这个函数。
我们加上inline试一下效果,看一下它的编译结果: 上面出现了一个链接错误,就是链接器在寻找Add函数定义的时候没有找到,所以导致了链接错误,就是在源文件主函数里面,找不到Add函数地址,出现的链接错误。
编译器将内联函数编译完成后,并没有生成函数体,而是在当前文件中(即函数的定义文件中),如果对内联函数进行调用,就会直接展开。
可以说明用inline修饰的内联函数具有内联函数文件作用域,即只能在定义的文件中使用。
我们内联函数的引入,就是基于宏函数存在的种种缺陷,但是内联函数到底有没有解决或者规避这些缺陷呢,我们也来做一个验证。 首先我们尝试一下内联函数有没有宏函数那样的副作用,以一个输出两者较大值的函数为例:
inline int Max(int left, int right) { return left > right ? left : right; } int main() { int a = 10; int b = 20; int c = 0; c=Max(++b, a); cout << c << endl; system("pause"); }我们来看一下输出结果: 可以看到输出结果为21,就是在b自加变为21之后,直接替换++b,10替换a,然后比较21号和10,输出21,而不是宏函数中的22,一定程度上避免了宏函数中的副作用。
其次我们可以看到内联函数中,是有定义变量类型的,这就意味着他在编译的时候会做类型检测的。
在定义的时候是整型,在调用的时候,输入一个字符串,出现了参数不兼容的问题,即内联函数这里进行了类型检测。
宏函数和内联函数存在着同样的一个缺陷——那就是会造成代码膨胀。 上面提到过inline是一种以时间换空间的做法。 假设Add函数有5行的代码,那么展开来就会有25行的代码,这样下来,不用去调用一个个的函数,展开计算5个函数就可以了,省了好多调用函数需要开辟的栈帧以及空间,这就是上面提到的,用时间换空间的做法。
但是这样做的话,无疑就会使代码变得膨胀,整个结构很冗余,因此也有利有弊。
关于内联函数的描述就到这里了,如果有需要,还会继续补充,希望能帮到你。
加油吧,阿超没有蛀牙。