1. 内联函数(inline function)
内联函数与C语言的宏函数有一定关系,在学习内联函数之前,我们先了解以下宏:
宏优点:以空间换时间
宏缺陷:
-
缺陷1:需要加括号保证运算的完整性
#define MYADD(x,y) ((x)+(y)) void test01(){ int ret = MYADD(10,20)*20; cout << "ret = "<< ret << endl; }
-
缺陷2:即使加了括号,有些情况依然与预期效果不符
#define MYCOMPARE(a,b) (((a) < (b)) ? (a) : (b)) void test01(){ int a = 10,b = 20; int ret = MYCOMPARE(++a,b); //ret = 12 !!!预期效果ret=11,没想到ret=12 //这是因为:(((++a) < (b)) ? (++a) : (b)) 故ret=12 }
为了应对宏缺陷,C++引入了内联函数
在C++中,内联函数(inline function) 是一种特殊的函数声明,告诉编译器在调用该函数时将函数的代码直接插入到调用点,而不是通过传统的函数调用机制(即通过栈帧跳转)。这种机制可以在某些场景下减少函数调用的开销,特别是对于小而频繁的函数。
内联函数本身也是一个真正的函数
基本概念:
- 内联(inline):表示编译器尽可能地在每个调用该函数的地方将函数的代码直接展开,而不是通过函数调用的方式。
- 函数调用的开销:在传统函数调用中,CPU 需要保存调用点的上下文(如寄存器、程序计数器),将参数传递给被调用函数,执行跳转,返回时还需要恢复上下文,这些都会产生额外的开销。对于简单的、频繁调用的函数,这种开销可能显得不必要。
- 内联展开:编译器将内联函数的代码直接插入调用点,避免了传统函数调用的开销,从而提高运行效率。
2. 定义内联函数
在C++中,可以通过在函数定义前加上 inline
关键字来建议编译器将该函数定义为内联函数。注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待;
inline int add(int a, int b) {
return a + b;
}
在这个例子中,add
函数被声明为内联函数,编译器可能会将 add
函数的代码直接嵌入到每次调用该函数的地方,而不是通过函数调用。
2.1 类内部的内联函数
为了定义内联函数,通常必须在函数定义前面放一个inline
关键字,但是在类内部定义内联函数时并不是必须的,任何在类内部定义的函数自动成为内联函数。
3. 内联函数的特性
- 避免函数调用开销: 对于小型函数,特别是那些在循环中频繁调用的函数,内联展开可以减少函数调用的时间开销,进而提升程序性能。
- 编译器建议:
inline
关键字只是给编译器的一个建议,并不强制要求编译器一定要将函数内联展开。编译器会根据具体情况决定是否内联。如果函数体较大、过于复杂或者递归调用等,编译器可能会忽略内联建议。 - 内联函数的定义必须放在头文件中: 内联函数通常需要放在头文件中,这样它的定义可以在多个编译单元中使用。因为内联函数在每个调用点都需要可见其定义,编译器才能将其内联展开。
- 避免代码膨胀: 虽然内联函数可以提高效率,但如果函数过大,或者频繁调用,内联展开会导致生成的可执行文件变得庞大(因为每个调用点都会插入一份函数的代码),这被称为代码膨胀。因此,内联函数适合用于小型、简单的函数。
以下情况编译器可能考虑不会将函数进行内联编译:
- 不能存在任何形式的循环语句
- 不能存在过多的条件判断语句
- 函数体不能过于庞大
- 不能对函数进行取址操作
内联仅仅只是给编译器一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联编译。一个好的编译器将会内联小的、简单的函数。
4. 内联函数的使用场景
- 简单的小函数:如getter、setter函数、数学计算函数等。它们往往功能简单、执行快速,内联化能带来性能提升。
- 频繁调用的函数:如果某个函数被多次调用,尤其是在循环中的调用,内联可以减少调用开销。
5. 内联函数的局限性
- 递归函数不能内联: 内联函数要求编译器将函数体直接插入到调用点,对于递归函数来说无法实现这种展开,编译器会忽略内联请求。
- 函数体过大时编译器会忽略内联请求: 内联函数适合小型函数,如果函数过大(如包含复杂的逻辑、大量代码等),编译器可能不会内联展开。
- 可能导致代码膨胀: 内联函数可能会导致代码膨胀。如果一个内联函数在多个地方被调用,编译器会在每个调用点都插入该函数的代码,可能导致生成的可执行文件增大,带来缓存压力。
示例:
#include <iostream>
using namespace std;
// 内联函数
inline int square(int x) {
return x * x;
}
int main() {
int a = 5;
cout << "Square of " << a << " is: " << square(a) << endl;
return 0;
}
在这个例子中,square
函数被声明为内联函数。每当调用 square(a)
时,编译器可能会将函数代码 x * x
直接插入到调用点,而不是执行传统的函数调用。
编译时可能的展开方式:
cout << "Square of " << a << " is: " << (a * a) << endl;
编译器在处理内联函数时,可能会将函数调用替换为其实际的实现,从而避免了函数调用的开销。
6. 内联函数与宏的对比
在C语言中,常常使用宏来实现类似于内联的效果。然而,C++中的内联函数相比宏具有以下优势:
- 类型检查:内联函数具有类型安全性,而宏没有类型检查。
- 调试支持:内联函数可以在调试时跟踪调用,而宏展开后无法调试。
- 作用范围明确:内联函数遵循作用域规则,而宏是简单的文本替换,不受作用域的限制。
宏和内联的对比示例:
#define SQUARE(x) ((x) * (x)) // 使用宏
inline int square(int x) { // 使用内联函数
return x * x;
}
宏虽然可以实现类似的功能,但在使用时容易出错,例如在传递复杂表达式时,宏会导致不可预期的行为,而内联函数则不会:
int result = SQUARE(3 + 4); // 宏展开为 ((3 + 4) * (3 + 4)),结果错误
int result = square(3 + 4); // 内联函数安全,结果正确
7. 总结
- 内联函数通过
inline
关键字建议编译器在调用点展开函数体,减少函数调用的开销。 - 它适用于小型、简单、频繁调用的函数,避免函数调用的栈开销。
- 由于编译器并不总是遵循内联建议,因此不能完全依赖
inline
来提升性能。 - 与宏相比,内联函数提供了更好的类型安全性和调试支持。