c++内联函数的概念
C++内联函数是一种编译器优化技术,它允许将函数的定义直接插入到调用处,而不是通过函数调用的方式执行。这样可以减少函数调用的开销,提高程序的执行效率。内联函数通过inline
关键字实现。
先看下没有使用内联函数的情况
int add(int x, int y) {
return x + y;
}
int main() {
add(1, 1);
return 0;
}
上面是cpp代码编译之后的汇编代码。解释如下:
首先,我们看到了一个名为__Z3addii
的标签,它表示函数add(int, int)
的起始位置。该函数将两个整数相加并返回结果。 在_main
函数中,我们看到了对add
函数的调用。通过将参数传递给寄存器esi
和edi
,然后调用call
指令,将控制权转移到add
函数。最关键的就是这个call指令,说明发生了函数地址调用。
加了inline
变成内联函数.
inline int add(int x, int y) {
return x + y;
}
int main() {
add(1, 1);
return 0;
}
对应的汇编代码如下:
push rbp
mov rbp, rsp
mov edi, 2
mov esi, 2
add edi, esi
mov eax, edi
pop rbp
xor eax, eax
ret
在这段汇编代码中,我们可以看到 add
函数被内联展开了。相应的 add
函数调用被替换为直接的指令序列,执行了加法运算。 具体来说, add(1, 1)
的调用被替换为以下指令:
mov edi, 1 ; 将参数 x 的值 1 存储到寄存器 edi
mov esi, 1 ; 将参数 y 的值 1 存储到寄存器 esi
add edi, esi ; 将寄存器 edi 和 esi 中的值相加
mov eax, edi ; 将寄存器 edi 中的值复制到寄存器 eax
最关键的是变成内联函数之后,没有了之前的call调用指令,而是被展开加到函数调用的地方。
推荐一个查看汇编的工具,Hopper
c++内联函数解决了什么问题
内联函数的引入主要是为了解决函数调用的开销问题。在调用普通函数时,需要保存当前函数的上下文,跳转到被调用函数的代码段执行,然后再返回到调用函数的位置。这个过程会带来一定的开销,特别是对于频繁调用的小型函数而言。
通过使用内联函数,编译器会将函数的代码直接插入到调用的地方,避免了函数调用的开销。这样可以节省时间和内存,提高程序的执行效率。但是需要注意的是,内联函数适用于函数体较小的函数,如果函数体较大,频繁使用内联函数可能会导致代码膨胀,反而降低程序的性能。
c++内联函数的特性
空间换时间
内联函数在编译阶段,把函数体展开加到调用处,这样替换了函数调用,所以说内联是一种空间换时间的做法。优点是减少函数调用开销,提高程序运行效率;缺点是可能会使目标文件变大。
编辑器对inline的处理
对于编译器而言,inline只是一个建议,编译器并不一定会做内联。编译器会根据一些优化策略来决定是否将函数内联。具体来说,编译器通常会将函数内联的条件如下:
- 函数体较小:如果函数体较大,内联函数会导致代码膨胀,反而会影响程序的执行效率。
- 频繁调用:如果函数很少被调用,内联函数也不会带来太大的效率提升。
- 不包含循环或递归:循环和递归会导致内联代码的复杂度增加,从而影响程序的执行效率。
inline声明和定义不能分离
内联函数的定义和声明通常放在一起。
test.h
inline int add2(int x,int y);
test.cpp
#include "test.h"
int add2(int x, int y) {
return x + y;
}
上面代码报错如下:
这是因为链接阶段错误. 因为使用
inline
后函数被展开, 不会call
相应函数的地址, 无法进行链接。解决办法:将内联函数的定义放在头文件中。
c++宏函数和内联函数的区别
在熟悉了内联函数的特性之后,有同学可能就在想c语言的宏不是也能实现类似的功能吗?那为什么不直接使用已有的宏函数呢?
- 宏函数:宏函数是通过
预处理器
进行处理的,它是一种简单的文本替换机制。在编译之前,预处理器会将宏函数的调用处替换为宏函数的定义内容。因此,宏函数没有实际的函数体,仅仅是简单的文本替换。 - 内联函数:内联函数是由
编译器
处理的。内联函数的定义和调用方式与普通函数相同,但是编译器会将内联函数的代码插入到调用处,而不是生成函数调用的指令。
看如下代码,使用宏函数就可能得到和预期不符的结果,明显用宏函数的结果是不对。
#define ADD(x, y) x + y
inline int add2(int x, int y) {
return x + y;
}
int main() {
int result = ADD(2, 3) * 2;
cout << result << endl; //输出:8
int result2 = add2(2, 3) * 2;
cout << result2<< endl; //输出:10
return 0;
}
宏函数还有如下缺点:
- 无法进行类型检查和编译器优化;
- 宏函数的定义和调用都是以文本替换的方式进行的,可能会导致代码可读性较差。在调试时,宏函数的替换结果可能会使调试过程变得困难。
所以c++建议优先考虑内联函数而不是宏函数。
函数调用的开销
函数调用开销的体现
函数调用的开销指的是在程序执行过程中,由于函数调用而产生的额外开销。这些开销包括以下几个方面:
-
栈帧的创建和销毁:当一个函数被调用时,需要在内存中创建一个栈帧,用于保存函数的局部变量、参数、返回地址等信息。而在函数调用结束后,这个栈帧需要被销毁。创建和销毁栈帧都需要一定的时间和资源。
-
参数传递:在函数调用时,需要将参数传递给被调用函数。对于较大的参数,可能需要进行复制或者引用传递,这也会带来一定的开销。
-
调用过程中的跳转:在函数调用时,需要跳转到被调用函数的代码段执行。这个跳转会引入额外的指令和时间消耗。
java中处理是否有所不同
C++是一种静态类型语言,需要提前编译。在编译过程中,代码被转换为机器码,函数调用直接解析。而Java是一种动态类型语言,代码被编译为字节码并由Java虚拟机(JVM)执行。JVM会进行即时编译(JIT)优化,可以在运行时优化函数调用。
Java的处理方式不太一样。因为Java在设计时采用了一种不同的函数调用方式,即通过虚拟机(JVM)来执行函数调用。在Java中,函数调用的开销主要由JVM负责处理,而不是由程序本身直接处理。JVM通过使用即时编译技术、内联优化等手段,可以在一定程度上减少函数调用的开销。此外,Java也提供了一些特性,如垃圾回收机制,可以自动管理内存,进一步减少了一些开销。需要注意的是,虽然Java中相对于C++来说函数调用的开销较小,但仍然存在一定的开销。