跳至主要內容

inline内联函数

张威大约 8 分钟c/c++c++基础

inline内联函数

1681304109194-2447f7ce-8687-48ed-932c-a4679599e78c
1681304109194-2447f7ce-8687-48ed-932c-a4679599e78c

在C++中,通常定义以下函数来求取两个整数的最大值

int max(int x, int y)
{
    return x > y ? x : y;
}

为这么一个小的操作定义一个函数的好处有:

  • 阅读时好理解:阅读和理解函数 max 的调用,要比读一条等价的条件表达式并解释它的含义要容易得多;
  • 修改时好找:如果需要做任何修改,修改函数要比找出并修改每一处等价表达式容易得多;
  • 使用函数可以确保统一的行为,每个测试都保证以相同的方式实现;
  • 方便重用:函数可以重用,不必为其他应用程序重写代码。

虽然有这么多好处,但是写成函数有一个潜在的调用函数比求解等价表达式要得多。在大多数的机器上,调用函数都要做很多工作。即对于这种简短的语句使用函数****。

函数调用过程(函数调用过程也被称为函数的开销):

从右向左先压实参,然后执行call指令,call指令先把call指令的下一行指令的地址入栈,然后进入被调函数,被调函数左括号{先把调用方的栈底地址入栈,然后再给被调函数开辟栈帧,对被调函数的栈帧初始化(gcc/g++只负责开辟栈帧不进行初始化!!!汇编指令可以看到),......

在C语言中,我们使用带参数的宏定义这种借助编译器的优化技术来减少程序的执行时间,那么在C++中有没有相同的技术或者更好的实现方法呢?答案是有的,那就是**内联(inline)函数**。内联函数作为编译器优化手段的一种技术,在降低运行时间上非常有用。

#include <iostream>

using std::cout;
using std::endl;

#define multiply(x, y) ((x) * (y))//带参数宏定义,x,y要看成表达式加(),否则容易出错
/* #define multiply(x, y) x * y */ //a + c * b + d = 3 + 5 * 4 + 6会出错

//普通函数会有参数入栈出栈的开销

//现代的编译器可以自动识别一个函数是不是内联函数
//特点:在函数调用的时候,用函数体去代替函数的调用
inline int add(int x, int y)
{
    return x + y;
}

int main(int argc, char **argv)
{
    int a = 3, b = 4, c = 5, d = 6;
    cout << "multiply(a, b) = " << multiply(a, b) << endl;
    //a + c * b + d = 3 + 5 * 4 + 6
    cout << "multiply(a + c, b + d) = " << multiply(a + c, b + d) << endl;
    
    cout << "add(a, b) = " << add(a, b) << endl;
    return 0;
}

什么是内联函数?

内联函数是C++的增强特性之一,用来降低程序的运行时间

内联函数:在编译过程(非程序运行阶段)中,就没了函数的调用开销了,在函数的调用点直接把函数的代码进行展开处理

  • inline函数不再生成相应的函数符号

    gcc -c main main.cpp -O2 
    objdump -t main.o
    
  • 所以可以通过判断有没有生成函数符号,

定义函数时,在函数的最前面以关键字“inline”声明函数,即可使函数称为内联声明函数

inline int max(int x, y)
{
    return x > y ? x : y;
}

inline内联函数和普通函数的区别🍗🍗🍗

面试中,简单问题不要只回答定义,要从原理上解释

  • 函数调用参数压栈,栈帧的开辟和回退过程,函数调用开销大
  • inline函数在编译过程中,没有函数调用开销了,在调用点直接把函数代码展开处理了
  • inline函数不再生成相应的函数符号
  • inline只是对编译器建议把函数处理成内联函数, 不是所有inline都会被编译器处理成内联函数,如递归,因为不知道展开几次
  • debug版本(-g)上inline无效,只在release版本可用

普通函数调用需要标准开销,但是内联函数不需要开销;如果这个函数在短时间内大量调用并且这个函数十分简单,我们尽量将其设置为内联函数;内联函数如果内联成功,不会在符号表生成符号。内联函数并不一定能够内联,最终由编译器决定

内联函数和带参数的宏定义

  1. 宏定义没有类型检查

无论是《Effective C++》中的 “Prefer consts,enums,and inlines to #defines” 条款,还是《高质量程序设计指南——C++/C语言》中的“用函数内联取代宏”。在书《高质量程序设计指南——C++/c语言》中这样解释到: image.pngimage.pngimage.png

将内联函数放入头文件

关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。下面的foo函数

inline void foo(int x, int y);//该语句在头文件中
void foo(int x, int y)//实现在.cpp文件中
{   
    //...   
}

而正确的姿势如下:

inline void bar(int x, in y)//该语句在头文件中
{   
    //...   
}

所以说,C++ inline函数是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。一般地,用户可以阅读函数的声明,但是看不到函数的定义尽管在大多数教科书中内联函数的声明、定义体前面都加了 inline 关键字,但我认为 inline 不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。 内联函数应该在头文件中定义,这一点不同于其他函数。编译器在调用点内联展开函数的代码时,必须能够找到 inline函数的定义才能将调用函数替换为函数代码,而对于在头文件中仅有函数声明是不够的。 当然内联函数定义也可以放在源文件中,但此时只有定义的那个源文件可以用它,而且必须为每个源文件拷贝一份定义(即每个源文件里的定义必须是完全相同的),当然即使是放在头文件中,也是对每个定义做一份拷贝,只不过是编译器替你完成这种拷贝罢了。但相比于放在源文件中,放在头文件中既能够确保调用函数是定义是相同的,又能够保证在调用点能够找到函数定义从而完成内联(替换)。

//add.h
#ifndef __ADD_H__
#define __ADD_H__

//对于内联函数(inline函数)而言,不要分成头文件与实现文件的形式,不能将声明与实现分开

//内联函数一般都是一些小函数,不要去写for/while这些复杂的语句
inline
int add(int x, int y);

#include "add.cc"//可以使用#include进行包含实现文件

#endif

谨慎地使用内联

内联能提高函数的执行效率,**为什么不把所有的函数都定义成内联函数?**以下摘自《高质量程序设计指南----C/C++语言》: image.png

课堂代码

//#include "add.h"

inline
int add(int x, int y)
{
    return x + y;
}
//#include "add.h"
#ifndef __ADD_H__
#define __ADD_H__

//对于内联函数(inline函数)而言,不要分成头文件与实现文件的形式
//不能将声明与实现分开
//可以使用#include进行包含实现文件

//内联函数一般都是一些小函数,不要去写for/while这些复杂的语句
inline
int add(int x, int y);

#include "add.cc"

#endif
#include "add.h"
#include <iostream>

using std::cout;
using std::endl;

int main(int argc, char **argv)
{
    int a = 3, b = 4;
    cout << "add(a, b) = " << add(a, b) << endl;
    return 0;
}