特别标注:本文部分内容源于网络,原作链接已标注于“参考阅读”部分,该作品使用 CC BY-SA 4.0 许可发布。本文亦遵循 CC BY-SA 4.0 许可。

回头看系列文章序
自大三起我认识到,随着应用知识的扩展,基础知识的重要性愈加明显。时至今日,已经到了无法忽视的地步,以至于我们必须采取有效措施,认真巩固语言基础、数据库、操作系统等一系列重要基础知识,将零散的知识点体系化,我将这一计划称之为“回头看”。

在C++编程中,inline关键字常被用来提示编译器将函数体直接插入到调用处,通过牺牲代码空间的方法,避免了函数调用的开销,赢得了运行时间。

什么是inline函数?

inline函数是一种建议编译器在调用该函数时,将函数体直接替换到调用位置,而不是进行正常的函数调用。这种优化旨在减少函数调用的开销,特别是对于那些频繁调用的小函数。

声明与定义

要声明一个inline函数,只需在函数定义前加上inline关键字:

inline int add(int a, int b) {
    return a + b;
}

也可以将其与类成员函数结合使用:

class Math {
public:
    inline int multiply(int a, int b) {
        return a * b;
    }
};

工作机制

当编译器遇到inline函数时,会尝试将该函数的代码展开到调用点,类似于宏替换,但不同于宏的是,inline函数在进行替换时会进行类型检查和语法检查

例如:

inline int square(int x) {
    return x * x;
}

int main() {
    int result = square(5);
    return 0;
}

编译器会将main函数中的square(5)替换为:

int main() {
    int result = 5 * 5;
    return 0;
}

inline的局限性和注意事项

  1. 使用限制inline只适合简单的函数使用,不能包含复杂的控制语句,且不能用于递归函数,因为递归调用本质上不能展开为内联代码。
  2. 编译器只是建议inline只是对编译器的建议,编译器不一定会按内联函数进行展开,尤其是当函数体过大或复杂时。
  3. 代码膨胀:大量使用inline函数可能导致生成的代码体积增大,因为每次调用都插入了完整的函数体。
  4. 调试困难:内联函数的展开可能会使调试过程变得复杂,因为实际执行的代码和源代码可能不完全一致。

隐式内联

现代编译器会智能的一些函数进行内联,即使不显式使用inline关键字,编译器也可能会自动将一些小函数内联展开。

  • 类的成员函数定义在类的声明中时,不需要inline关键字。

例如在结构体中的情形:

#include <iostream>
struct User
{
	char name[256];
    int age;
    void who (void){
	    std::cout << "我是" << name <<",今年" << age << "岁。" << std::endl;
    }
};
    
int main(int argc, const char * argv[]){
	User user = {"张飞", 25};
    user.who();
    return 0;
}

函数who被声明和定义在结构体User中。该函数会被自动优化为内联函数,成为隐式内联。函数的函数体被编译形成的二进制码将直接替代上述代码中的user.who(),也即:

void who (void){
    std::cout << "我是" << name <<",今年" << age << "岁。" << std::endl;
    }

宏定义与普通函数

代替普通函数:提高程序运行效率。

普通函数频繁调用的问题

函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。

栈空间是用于存放程序的局部变量和函数内部数据的内存区域。在计算机系统中,栈空间是有限的。如果频繁且大量地使用栈空间,可能会导致栈空间不足,从而引发程序错误。特别是在函数递归调用时,如果递归深度过大,最终会耗尽栈内存,导致程序崩溃。

当一个函数的代码较多且执行时间较长时,函数调用所花费的时间可以忽略不计。然而,如果一个函数只有一两条简单的语句,那么函数调用本身的开销(如参数传递、栈帧创建和销毁等)就变得相对显著,影响程序的执行效率。

内联函数解决的问题

  1. 减少函数调用开销:函数调用需要保存和恢复寄存器、传递参数等,inline函数可以避免这些开销。
  2. 代码优化:对于一些简单的、频繁调用的小函数,inline可以让编译器进行更好的优化。

宏定义与内联

内联函数的最初目的:代替部分#define宏定义。

  • 宏定义:预处理指令,在预处理时对所有宏进行替换;
  • 内联函数:函数,在编译阶段把有调用内联函数的地方进行插入;

为什么要用内联函数代替宏定义?

  1. 与宏相比,inline函数提供了更安全和可读的代码,因为它们受C++的类型和语法检查约束
  2. 宏的编写有很多限制,例如只能写一行,不能使用return控制流程等;
  3. 无法操作类的私有数据成员。

总结

inline关键字是C++中用于优化函数调用的一个有用工具,但应谨慎使用。对于频繁调用的小型函数,inline可以提高性能;但对于大型或复杂的函数,内联可能会导致代码膨胀和维护困难。了解编译器的行为和优化策略,可以帮助开发者更好地利用inline来提升程序性能。

参考阅读