C++之内联函数详解
内联函数的概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调 用建立栈帧的开销,内联函数提升程序运行的效率。
为什么有内联函数?
在详细的讲解内联函数之前我们要思考一下为什么会存在内联函数?下面我们以快排算法为例,为大家讲解一下为什么要存在内联函数。
不了解快排的也不需要担心,我们只需要知道在快排中,我们需要频繁的调用swap函数
当我们要使用快排(qsort)时,我们要调用swap函数,而调用函数是需要建立栈帧的!所以数据量一大!我们就要不断的建立栈帧!而频繁建立栈帧这件事本身也是有性能消耗的!
所以为了进一步的优化性能我们该结局和这个问题呢?
在c语言中,我们可以使用==宏函数==或者直接==写在qsort函数里面==!
直接写在里面就不需要调用函数建立栈帧!但是直接写在里面就失去了复用性!
所以为了具有更高的复用性使用宏函数是更好的选择!
因为宏函数的本质是替换,会在预编译阶段就进行替换也相当于直接写在里面!
但是宏函数本身也是有很多缺点的!
- 不能调试!这就是一件非常麻烦的事情,也就是一旦出错我们可能要花很长时间才能发现问题在哪!
- 没有类型安全的检查
- 宏函数很容易写错!
所以c++为了解决这个麻烦提出来内联函数这个概念!它解决了宏函数的所有缺点!又保留了它的所有优点!
内联函数的使用
inline int swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}//这就是内联函数!
内联函数只会在需要的时候进行展开!
什么叫展开?
int add(int x,int y)
{
return x+y;
}
int main()
{
add(1,3);
return 0;
}
这是不加inline的函数在反汇编情况下
内联函数的特点
-
内联函数只是对编译器发出的一个建议!编译器可以选择忽略这个请求!
那么什么情况下不适合使用内联呢?——递归和长函数的时候都不适合使用内联!
我们下面举个例子
假设一个func函数有在汇编中30个指令,而这个函数有10000个调用的地方!
那么请问展开和不展开的指令数分别是多少?
我们已知不展开的时候编译器会发出==call指令==去调用函数建立栈帧!10000个调用的地方就说明我们要调用10000次,说明==call了一万次==,调用完后再回来,但是func本身所含有的的指令数是不变的!所以总的指令数为10000+30 = 10030个
如果我们进行展开呢?那我们10000个调用的地方就要展开10000次,那么一次30个指令数,一共有10000*30 = 30w个指令数!
==这种现象就被我们称为代码膨胀!==就是无用重复的代码太多了!
而这会导致什么后果?——==可执行程序大小会膨胀变大==!就是我们常说的安装包!所以内联函数是不可以随便乱用的!
现在我们回头看递归和长函数,长函数一旦多次展开就导致代码膨胀!而递归如果深度一深,也会疯狂的进行展开!同样会导致代码膨胀!
所以inline真正适合的是需要多次调用的,非递归的,函数规模较小(函数很短)的函数。
可以用于减少开销,提高程序运行效率!而目标文件大小又不会过度膨胀
所以为了防止内联被乱用,内联被使用的使用要经过编译器的判断!如果函数过长或者是个递归,编译器可以忽略这个请求!
-
inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会 用函数体替换函数调用,缺陷:可能会使目标文件变大——就是上面所说的代码膨胀!优势:少了调用开销,提高程序运 行效率。
-
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
//a.h中 inline test(int x,int y); //a.cpp中 inline int test(int x, int y) { return x + y; } //test.cpp int main() { test(1,2); return 0; }
我们会得到这样的报错!就好像没有被声明一样!
先看==没有加inline==的样子
当我进行预编译的时候.h文件会进行展开!变成下面的样子!然后执行程序
//test.cpp int test(int x,int y); int main() { test(1,2); return 0; }
执行程序后此时我们看反汇编
我们可以看到当调用test函数的时候旁边是有一堆数字的,这一堆数字就是他的地址!就是通过这个地址我们才找到这个函数!
但是如果我们加入了inline这个位置就变成了一个?没有了地址!完全找不到了!
相当于我们只有一个声明!没有地址!只有声明的话编译器是可以通过的!但是没有地址链接器就无法进行链接!而一般来说这个地址是放在符号表里面的!链接器通过这个名字去符号表里面去找相应的地址!然后对不同的源文件进行链接!
那为什么加了inline就找不到了?==因为对于内联函数来说,它的作用是在需要的时候进行展开!没有人会去call内联函数的地址!==所以 内联函数根本不会去生成什么建立栈帧的指令,也没有必要把他的地址放进符号表里面!
这就导致了一旦内联函数声明和定义分离开来,就会导致链接错误!
所以最好的办法将声明的定义放在同一个头文件里面!
那么这里又有一个小问题?
我们都知道inline只是一个建议,当函数过长或者为递归的时候,编译器可以忽略这个建议,那么问题来了?如何我们把一个很长的函数(编译器一定会忽略这个建议)前面加个inline 然后声明和定义分离,那是不是就可以通过了?
//a.cpp中
inline int test(int x, int y)
{
int a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
a = x + y;
}
//a.h中
inline test(int x,int y);
//test.cpp
int main()
{
test(1,2);
return 0;
}