定义

在 C++ 中,内联函数(Inline Function)是一种特殊的函数。它是一种以inline关键字修饰的函数,例如:

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

当编译器处理对内联函数的调用时,会尝试将函数体直接插入到调用该函数的地方,而不是像普通函数那样进行函数调用的过程(包括参数压栈、跳转、返回等操作)。

作用

提高程序性能减少函数调用的开销。在普通函数调用时,会涉及到一系列的操作,如保存当前的执行上下文(包括程序计数器、寄存器等的值)、将参数压入栈中、跳转到函数的入口地址、执行函数体、将返回值存储到合适的位置、恢复之前保存的执行上下文等。这些操作在函数频繁调用时会产生一定的时间和空间开销。

例如,考虑一个简单的函数square,用于计算一个整数的平方:

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

如果在一个循环中频繁调用这个函数:

for (int i = 0; i < 1000; ++i) {
    int result = square(i);
    // 其他操作
}

每次调用square函数都会产生函数调用开销。如果将square函数定义为内联函数(inline int square(int x)),编译器可能会将函数体直接插入到循环内部的调用位置,这样就避免了频繁的函数调用开销,从而可能提高程序的执行速度。

保持代码的可读性和结构化

内联函数允许程序员把复杂的计算或者操作封装成一个类似函数的形式,即使这个 “函数” 在代码中被频繁使用。这样可以让主程序逻辑更加清晰,就像使用普通函数一样,通过有意义的函数名来传达操作的意图。

例如,有一个计算两点之间距离的函数:

inline double distance(double x1, double y1, double x2, double y2) {
    return std::sqrt((x2 - x1)*(x2 - x1)+(y2 - y1)*(y2 - y1));
}

在处理几何计算相关的代码时,可以直接使用这个distance函数,使代码看起来更有条理,同时又能获得内联函数带来的性能优势。

注意事项

虽然内联函数有性能优势,但并不是所有的函数都适合定义为内联函数。

编译器对于内联函数有自己的判断标准。如果函数体非常庞大,编译器可能会忽略inline关键字,仍然按照普通函数的方式来处理。因为如果函数体过大,直接插入到调用位置可能会导致代码膨胀,增加程序的内存占用等问题。

另外,内联函数的定义(包括函数体)通常需要放在头文件中,这样在多个源文件使用该内联函数时,编译器才能在每个调用点都能获取到函数体进行内联展开。

内联函数的几个问答

函数前面加上inline一定会有效果吗?

答:不会,使用内联inline关键字修饰函数只是一种提示,编译器不一定认。

如果不加inline就不是内联函数了吗?

答:存在隐式内联,不用inline关键字,C++中在类内定义的所有函数都自动称为内联函数。

内联函数一定就会展开吗?

答:其实和第一个问题类似,还是看编译器认不认。

在什么情况下内联函数会展开?

答:首先需要满足有inline修饰或者是类中的定义的函数,然后再由编译器决定。

  1. 隐式内联:

C++中在类内定义的所有函数都自动称为内联函数,类的成员函数的定义直接写在类的声明中时,不需要inline关键字

#include <stdio.h>
 
class Trace{
public:
	Trace()
	{
		noisy = 0;
	}
	void print(char *s)
	{
		if (noisy)
		{
			printf("%s", s);
		}
	}
	void on(){ noisy = 1; }
	void off(){ noisy = 0; }
private:
	int noisy;
};

显示内联:需要使用inline关键字

#include <stdio.h>
 
class Trace{
public:
	Trace()
	{
		noisy = 0;
	}
	void print(char *s); //类内没有显示声明
	void on(){ noisy = 1; }
	void off(){ noisy = 0; }
private:
	int noisy;
};
//类外显示定义
inline void Trace::print(char *s)
{
	if (noisy)
	{
		printf("%s", s);
	}
}

内联函数与普通函数的区别

调用方式

普通函数:当调用普通函数时,程序会将控制权转移到被调用函数的入口地址。在这个过程中,需要进行一系列操作,如将参数压入栈中、保存当前的执行上下文(包括程序计数器、寄存器的值等),执行完函数后再恢复执行上下文,将返回值存储在合适的位置。例如:

int add(int a, int b) {
    return a + b;
}
int main() {
    int x = 3, y = 4;
    int result = add(x, y);  // 调用普通函数add
    return 0;
}

内联函数:编译器在编译阶段会尝试将内联函数的代码直接嵌入到调用该函数的地方,就好像把函数体中的代码直接写在调用处一样。这样可以减少函数调用的开销。例如:

inline int add(int a, int b) {
    return a + b;
}
int main() {
    int x = 3, y = 4;
    int result = add(x, y);  // 编译器可能将add函数体直接嵌入此处
    return 0;
}

性能开销

普通函数:由于存在函数调用的开销,包括参数传递、栈操作、返回地址的保存和恢复等操作,对于频繁调用的小函数,这些开销可能会对程序性能产生一定的影响。

内联函数:在理想情况下,内联函数可以减少函数调用的开销,提高程序的执行速度。但如果内联函数体本身很复杂、代码量很大,可能会导致代码膨胀,增加内存占用,反而可能影响性能。

代码大小

普通函数:无论在多少个地方调用普通函数,函数代码在内存中只有一份,不同的调用点通过函数指针来访问。所以不会因为函数调用次数多而显著增加代码大小。

内联函数:每一个内联函数的调用点都会插入函数体代码,这可能会使代码体积增大。例如,如果一个内联函数体有 10 行代码,并且在 100 个地方调用它,那么最终的代码可能会比使用普通函数多出近 1000 行(假设编译器完全内联展开)。

编译过程中的处理方式

普通函数:编译器会单独编译函数的定义,生成函数的目标代码,在链接阶段将调用函数的代码和函数本身的目标代码进行链接。

内联函数:编译器在编译调用内联函数的源文件时,需要能够访问到内联函数的定义,以便进行内联展开。这通常意味着内联函数的定义(包括函数体)要放在头文件中,或者在调用之前已经有了完整的内联函数定义。

定义和使用内联函数

定义内联函数可以使用inline关键字来定义内联函数。定义方式如下:

inline 返回类型 函数名(参数列表) {
    // 函数体
}

例如,定义一个简单的内联函数来计算两个整数的最大值:

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

使用内联函数和普通函数一样,在需要使用内联函数的地方直接调用即可。例如:

int main() {
    int num1 = 5, num2 = 3;
    int larger_num = max(num1, num2);  // 使用内联函数max
    return 0;
}

注意事项

虽然使用inline关键字建议编译器将函数内联展开,但编译器有权决定是否真正内联一个函数。例如,如果函数体非常复杂(包含循环、递归等复杂结构)或者编译器的优化策略认为不适合内联,编译器可能会忽略inline关键字,将函数当作普通函数处理。

内联函数的定义通常应该放在头文件中。因为在编译每个使用内联函数的源文件时,编译器需要看到内联函数的完整定义才能进行内联展开。如果只在源文件中定义内联函数,而在其他源文件中调用它,编译器可能无法进行内联处理。例如,将上述max函数的定义放在一个名为utility.h的头文件中,然后在多个源文件中包含这个头文件就可以在这些源文件中使用max函数并且编译器有机会进行内联展开。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部