C++内联函数详解

这里写目录标题

  • 内联函数
  • 内联函数的定义
  • 将内联函数的定义放入头文件
  • 滥用内联的缺点
  • 递归函数和虚函数的内联
  • 内联函数和宏的区别

内联函数

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

int max(int a, int b)
{
 return a > b ? a : b;
}

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

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

虽然有这么多好处,但是写成函数有一个潜在的缺点:调用函数比求解等价表达式要慢得多。在大多数的机器上,调用函数都要做很多工作:调用前要先保存寄存器,并在返回时恢复,复制实参,程序还必须转向一个新位置执行

C++中支持内联函数,其目的是为了提高函数的执行效率,用关键字 inline 放在函数定义(注意是定义而非声明,下文继续讲到)的前面即可将函数指定为内联函数,内联函数通常就是将它在程序中的每个调用点上“内联地”展开,假设我们将 max 定义为内联函数:

inline int max(int a, int b)
{
 return a > b ? a : b;
}

cout<在编译时会展开为cout<<(a > b ? a : b)<从而消除了把 max写成函数的额外执行开销

内联函数的定义

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

如下风格的函数 Foo 不能成为内联函数:

inline void Foo(int x, int y);   // inline 仅与函数声明放在一起  
void Foo(int x, int y)
{
 ...
}

而如下风格的函数 Foo 则成为内联函数:

void Foo(int x, int y);  
inline void Foo(int x, int y)   // inline 与函数定义体放在一起
{
 ...
}

所以说,C++ inline函数是一种用于实现的关键字,而不是一种用于声明的关键字

定义在类声明之中的成员函数将自动地成为内联函数,例如:

class A
{ 
public:
 void Foo(int x, int y) { ... }   // 自动地成为内联函数 
}

但是编译器是否将它真正内联则要看 Foo函数如何定义。

将内联函数的定义放入头文件

内联函数应该在头文件中定义,这一点不同于其他函数。编译器在调用点内联展开函数的代码时,必须能够找到 inline 函数的定义才能将调用函数替换为函数代码,而对于在头文件中仅有函数声明是不够的。

当然内联函数定义也可以放在源文件中,但此时只有定义的那个源文件可以用它,而且必须为每个源文件拷贝一份定义(即每个源文件里的定义必须是完全相同的),当然即使是放在头文件中,也是对每个定义做一份拷贝,只不过是编译器替你完成这种拷贝罢了。但相比于放在源文件中,放在头文件中既能够确保调用函数是定义是相同的,又能够保证在调用点能够找到函数定义从而完成内联(替换)。

但是你会很奇怪,重复定义那么多次,不会产生链接错误?我们来看一个例子:

A.h 
class A
{
public:
 A(int a, int b) : a(a),b(b){}
 int max();
private:
 int a;
 int b;
};
A.cpp 
#include "A.h"
inline int A::max()
{
 return a > b ? a : b;
}
Main.cpp 
#include 
#include "A.h"
using namespace std;
inline int A::max()
{
 return a > b ? a : b;
}

int main()
{
 A a(3, 5);
 cout<<a.max()<<endl;
 return 0;
}

一切正常编译,输出结果:5

倘若你在Main.cpp中没有定义max内联函数,那么会出现链接错误:找不到函数的定义。所以内联函数可以在程序中定义不止一次,只要 inline 函数的定义在某个源文件中只出现一次,而且在所有源文件中,其定义必须是完全相同的就可以。

在头文件中加入或修改 inline 函数时,使用了该头文件的所有源文件都必须重新编译。

复杂的内联函数的定义, 应放在后缀名为 -inl.h 的头文件中。

如果内联函数的定义比较短小, 逻辑比较简单, 实现代码放在 .h 文件里没有任何问题。 出于编写者和调用者的方便, 复杂的内联函数的定义, 应放在后缀名为 -inl.h 的头文件中。这样把实现和类定义分离开来, 当需要时包含对应的 -inl.h 即可。

滥用内联的缺点

滥用内联将导致程序变慢.。内联可能使目标代码量或增或减,这取决于内联函数的大小。内联非常短小的存取函数通常会减少代码大小,但内联一个相当大的函数将戏剧性的增加代码大小。

递归函数和虚函数的内联

递归函数不应该声明成内联函数,递归调用堆栈的展开并不像循环那么简单, 内联函数需要在编译器展开,但是递归层数在编译时可能是未知的

虚函数可以是内联函数,但是当虚函数表现多态性的时候不能正常内联。这是因为内联是在编译期,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联

inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

代码示例如下:

    #include 
    using namespace std;
    // 基类
    class Base{
        public:
            inline virtual void who(){
                cout << "I am Base\n";
            }
            virtual ~Base(){}
    };
    // 派生类
    class Derived:public Base{
        public:
            inline void who(){   // 不写inline时隐式内联
                cout << "I am Derived\n";
            }
    };
int  main(){
    // 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。
    Base b;
    b.who();
    // 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。
    Base *bptr = new Derived();
    bptr->who();
    // 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
    delete bptr;
    bptr = nullptr;
    return 0;
    
  }

内联函数和宏的区别

  1. 宏是预处理指令,在预处理的时候把所有的宏名用宏体来替换;内联函数是函数,在编译阶段把所有调用内联函数的地方把内联函数插入。
  2. 宏没有类型检查,无论对还是错都是直接替换;而内联函数在编译时进行安全检查。

你可能感兴趣的:(C++基础知识,内联函数)