掘金 阅读 ( ) • 2024-04-03 16:49

c++内联函数的概念

C++内联函数是一种编译器优化技术,它允许将函数的定义直接插入到调用处,而不是通过函数调用的方式执行。这样可以减少函数调用的开销,提高程序的执行效率。内联函数通过inline关键字实现。

先看下没有使用内联函数的情况

int add(int x, int y) {
    return x + y;
}

int main() {
    add(1, 1);
    return 0;
}

image.png

上面是cpp代码编译之后的汇编代码。解释如下:
首先,我们看到了一个名为 __Z3addii 的标签,它表示函数 add(int, int) 的起始位置。该函数将两个整数相加并返回结果。 在 _main 函数中,我们看到了对 add 函数的调用。通过将参数传递给寄存器 esiedi ,然后调用 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只是一个建议,编译器并不一定会做内联。编译器会根据一些优化策略来决定是否将函数内联。具体来说,编译器通常会将函数内联的条件如下:

  1. 函数体较小:如果函数体较大,内联函数会导致代码膨胀,反而会影响程序的执行效率。
  2. 频繁调用:如果函数很少被调用,内联函数也不会带来太大的效率提升。
  3. 不包含循环或递归:循环和递归会导致内联代码的复杂度增加,从而影响程序的执行效率。

inline声明和定义不能分离

内联函数的定义和声明通常放在一起。

test.h

inline int add2(int x,int y);

test.cpp

#include "test.h"  
  
int add2(int x, int y) {  
return x + y;  
}

上面代码报错如下:

image.png

这是因为链接阶段错误. 因为使用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++建议优先考虑内联函数而不是宏函数。

函数调用的开销

函数调用开销的体现

函数调用的开销指的是在程序执行过程中,由于函数调用而产生的额外开销。这些开销包括以下几个方面:

  1. 栈帧的创建和销毁:当一个函数被调用时,需要在内存中创建一个栈帧,用于保存函数的局部变量、参数、返回地址等信息。而在函数调用结束后,这个栈帧需要被销毁。创建和销毁栈帧都需要一定的时间和资源。

  2. 参数传递:在函数调用时,需要将参数传递给被调用函数。对于较大的参数,可能需要进行复制或者引用传递,这也会带来一定的开销。

  3. 调用过程中的跳转:在函数调用时,需要跳转到被调用函数的代码段执行。这个跳转会引入额外的指令和时间消耗。

java中处理是否有所不同

C++是一种静态类型语言,需要提前编译。在编译过程中,代码被转换为机器码,函数调用直接解析。而Java是一种动态类型语言,代码被编译为字节码并由Java虚拟机(JVM)执行。JVM会进行即时编译(JIT)优化,可以在运行时优化函数调用。

Java的处理方式不太一样。因为Java在设计时采用了一种不同的函数调用方式,即通过虚拟机(JVM)来执行函数调用。在Java中,函数调用的开销主要由JVM负责处理,而不是由程序本身直接处理。JVM通过使用即时编译技术、内联优化等手段,可以在一定程度上减少函数调用的开销。此外,Java也提供了一些特性,如垃圾回收机制,可以自动管理内存,进一步减少了一些开销。需要注意的是,虽然Java中相对于C++来说函数调用的开销较小,但仍然存在一定的开销。