【C++】inline函数(内联函数)

发布于:2024-12-20 ⋅ 阅读:(13) ⋅ 点赞:(0)


前言

C程序频繁调用函数会使代码效率降低,因为创建函数栈帧需要消耗时间。于是C语言引入宏函数的概念,使用宏函数来替代一些功能简单的函数,宏函数会在预处理时替换展开,就不需要建立栈帧,可以提高代码效率。但使用宏函数需要注意运算符优先级等问题很容易出错,且不方便调试,不好用。于是C++引入inline的概念,设计inline的目的就是替代C语言的宏函数,它使用起来更方便,且不易出错。


一、宏函数的缺陷

示例(把普通ADD函数改成宏函数的实现方式):

#include<iostream>
using namespace std;

int ADD(int a, int b)//普通ADD函数
{
	return a + b;
}

int main()
{
	cout << ADD(1, 2) << endl;
	cout << ADD(1, 2) * 5 << endl;
	return 0;
}

在这里插入图片描述
那么如何把上述代码中ADD函数改成宏函数的实现方式呢,我们很容易想当然的写成如下形式:

#include<iostream>
using namespace std;

#define ADD(a, b) a + b

int main()
{
	cout << ADD(1, 2) << endl;
	cout << ADD(1, 2) * 5 << endl;//展开是:1 + 2 * 5
	return 0;
}

在这里插入图片描述
cout << ADD(1, 2) * 5 << endl; 的打印结果出现问题。
因为宏函数在预处理时是直接把ADD(1, 2)替换成 1+2,由于运算符优先级2先和5相乘再加1,得到的结果就出现问题了。那么为了避免出现这种问题,应该在给a + b外面加一个括号。如下:

#include<iostream>
using namespace std;

#define ADD(a, b) (a + b) //给a + b外面加一个括号

int main()
{
	cout << ADD(1, 2) << endl;
	cout << ADD(1, 2) * 5 << endl;//给a + b外面加了括号后得到了正确的结果

	ADD(5 & 4, 1 | 2);//展开为:(5 & 4 + 1 | 2)
	return 0;
}

在这里插入图片描述
ADD(5 & 4, 1 | 2); 该宏函数还是不能正确计算这种式子的结果。
我们希望ADD函数把 5&4 的结果和 1|2 的结果进行相加,但由于展开后 ‘+’ 的优先级高于 ‘&‘和’|’ 运算符,会先进行加法,那么得到的结果就会出现问题了。为了避免这个问题,还得给 a 和 b 分别再套一个括号。如下:

#include<iostream>
using namespace std;

#define ADD(a, b) ((a) + (b)) //给 a 和 b 分别再套一个括号

int main()
{
	cout << ADD(1, 2) << endl;
	cout << ADD(1, 2) * 5 << endl;

	ADD(5 & 4, 1 | 2);//展开为:((5 & 4) + (1 | 2))
	return 0;         //防止了因运算符优先级引发的问题
}

总结:C语言实现宏函数会在预处理时替换展开,就不需要建立栈帧,就可以提高效率。但是宏函数实现需要注意运算符优先级等问题很容易出错的,且不方便调试,所以不是特别好用。于是C++引入了inline的概念,设计inline的目的就是替代C语言的宏函数,它使用起来更加方便,而且不易出错。下面展开介绍。

二、inline函数

1.inline函数的展开规则

用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率。

注:inline对于编译器而言只是⼀个建议,也就是说,你给函数加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。

下面我要用vs2022演示inline函数在什么情况展开(在debug版本下调试代码,然后观察代码的汇编指令来确定inline函数是否展开)

注:vs编译器为了方便程序员调试代码,设置了debug版本下面默认不展开inline函数,那么我们就无法观察inline函数在什么情况展开了。为了正常观察inline函数的展开情况,我们需要修改vs编译器的一些设置,这样debug版本想也能正常展开inline函数了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

设置好vs编译器后,在调试时观察以下代码的汇编指令:

代码一(代码短的inline函数会展开):

inline int Add(int x, int y)//一般来说代码短的inline函数会展开
{
	int ret = x + y;
	ret++;
	ret++;
	ret++;
	return ret;
}

int main()
{
	int ret = Add(1, 2);// 可以通过汇编观察程序是否展开
                        // 有call Add语句就是没有展开,没有就是展开了
	return 0;
}

在这里插入图片描述
没有发现call Add语句,证明Add函数直接在此展开了

补充:call Add语句的意思是调用Add函数。如果发现了call Add语句,就会调用Add函数,然后跳转到Add函数,创建Add函数的栈帧;如果没有发现call Add语句,证明Add函数直接在此展开了,可以直接在此执行Add函数里的指令,就不需要调用Add函数以及创建Add函数的栈帧,可以提升代码效率。

代码二(代码较长的inline函数不展开):

inline int Add(int x, int y)//故意加长inline函数的代码长度
{
	int ret = x + y;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	return ret;
}

int main()
{
	int ret = Add(1, 2);
	return 0;
}

在这里插入图片描述
发现call Add语句,证明Add函数没有在此展开,而是跳转到Add函数去执行后续相加的操作了。

2.inline的设计分析

既然inline这么好用,可以提升代码效率,而且还不会出错。那么为什么把它设计成只能展开代码比较短的函数呢?设计成加了inline的所有函数都会展开,对代码效率的提升岂不是更大吗?
这么做确实可以进一步提升效率,但会显著增加编译成的可执行程序的代码长度,inline实际上是用以空间换时间的方式来提升代码的效率。

举个例子:
在这里插入图片描述
所以要把inline设计成只能展开代码比较短的函数,这样就能保证inline既不会对可执行程序的大小造成比较大的影响,又可以提升代码的运行效率。

3.inline函数不建议声明和定义分离

inline函数不建议声明和定义分离到两个文件,分离会导致链接错误。

(1)先观察一下普通函数声明和定义分离的情况
在这里插入图片描述
在编译过程中,每个C++文件都会生成一个符号表,用来存储这个C++文件里所有函数的函数名及其地址。在链接阶段,所有obj文件和链接库⼀起链接生成最终的可执行程序(exe程序),同时所有编译过程中形成的符号表合并成一个符号表,供exe程序使用。
在这里插入图片描述
在合并后的符号表中确实找到了 f 函数的有效地址,所以代码正常运行。

(2)再观察inline函数声明和定义分离的情况
在这里插入图片描述
在这里插入图片描述

总结:inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为编译器认为inline函数会直接在使用的地方展开,inline函数的地址就不会被放进符号表,符号表中没有inline函数的有效地址,链接时就会报错。



网站公告

今日签到

点亮在社区的每一天
去签到