3.内联函数(了解)
1.内联函数的引出
编译过程的最终产品是可执行程序-------由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存。计算机随后将逐步执行这些指令。有时(如有循环或分支语句时),将跳过一些指令,向前或向后跳到特定地址。当执行到函数调用指令时,程序会将当前指令的内存地址保存到一个特殊的地方,称为调用栈(call stack)或堆栈(stack)。然后,程序会将函数参数复制到堆栈中,以便函数在执行时可以使用这些参数。接下来,程序会跳转到函数定义的位置,即函数起点的内存单元。函数代码在此处执行,期间可能会修改堆栈中的数据或使用其他寄存器。当函数执行结束时,程序会跳回到保存的指令处,即调用栈中保存的地址。此时,程序会恢复到函数调用之前的状态,并且可以使用函数返回的值(返回值放入寄存器)。这个过程被称为函数调用返回序列(function call return sequence),它确保了程序在函数调用和返回之间的状态正确性和一致性。来回跳跃并记录条约位置意味着以前使用函数时,需要一定开销。
C++从C中继承的一个重要特征就是效率。假如C++的效率明显低于C的效率,那么就会有很大的一批程序员不去使用C++了。
在C中我们经常把一些短并且执行频繁的计算写成宏,而不是函数,这样做的理由是为了执行效率,宏可以避免函数调用的开销,这些都由预处理来完成。
但是在C++出现之后,使用预处理宏会出现两个问题:
■第一个在C中也会出现,宏看起来像一个函数调用,但是会有隐藏一些难以发现的错误。
■ 第二个问题是C++特有的,预处理器不允许访问类的成员,也就是说预处理器宏不能用作类的成员函数。
为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,C++引入了内联函数(inline function).
内联函数为了继承宏函数的效率,没有函数调用时的开销,然后又可以像普通函数那样,可以进行参数,返回值类型的安全检查,又可以作为成员函数。
2.预处理宏的缺陷
预处理器宏存在问题的关键是我们可能认为预处理器的行为和编译器的行为是一样的。当然也是由于宏函数调用和函数调用在外表看起来是一样的,因为也容易被混淆。但是其中也会有一些微妙的问题出现:
问题一:
#define ADD(x,y) x+y
inline int Add(int x,int y)
{
return x + y;
}
void test()
{
int ret1 = ADD(10, 20) * 10; //希望的结果是300
int ret2 = Add(10, 20) * 10; //希望结果也是300
cout << "ret1:" << ret1 << endl; //210
cout << "ret2:" << ret2 << endl; //300
}
问题二:
#define COMPARE(x,y) ((x) < (y) ? (x) : (y))
int Compare(int x,int y)
{
return x < y ? x : y;
}
void test02()
{
int a = 1;
int b = 3;
//cout << "COMPARE(++a, b):" << COMPARE(++a, b) << endl; // 3
cout << "Compare(int x,int y):" << Compare(++a, b) << endl; //2
}
问题三:
预定义宏函数没有作用域概念,无法作为一个类的成员函数,也就是说预定义宏没有办法表示类的范围。
3.内联函数
3.1内联函数基本概念
在C++中,预定义宏的概念是用内联函数来实现的,而内联函数本身也是一个真正的函数。内联函数具有普通函数的所有行为。唯一不同之处在于内联函数会在适当的地方像预定义宏一样展开,所以不需要函数调用的开销。因此应该不使用宏,使用内联函数。
■在普通函数(非成员函数)函数前面加上inline关键字使之成为内联函数。但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。
inline void func(int a);
以上写法没有任何效果,仅仅是声明函数,应该如下方式来做:
inline int func(int a){return a++;}//函数代码行数过多就不太适合做内联函数
注意: 编译器将会检查函数参数列表使用是否正确,并返回值(进行必要的转换)。这些是预处理器无法完成的。
内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间。
3.2类内部的内联函数
为了定义内联函数,通常必须在函数定义前面放一个inline关键字。但是在类内部定义内联函数时并不是必须的。任何在类内部定义的函数自动成为内联函数。
class Person
{
public:
Person(){ cout << "构造函数!" << endl; }
void PrintPerson(){ cout << "输出Person!" << endl; }
}
构造函数Person,成员函数PrintPerson在类的内部定义,自动成为内联函数。
3.3内联函数和编译器
内联函数并不是何时何地都有效,为了理解内联函数何时有效,应该要知道编译器碰到内联函数会怎么处理?
对于任何类型的函数,编译器会将函数类型(包括函数名字,参数类型,返回值类型)放入到符号表中。同样,当编译器看到内联函数,并且对内联函数体进行分析没有发现错误时,也会将内联函数放入符号表。
当调用一个内联函数的时候,编译器首先确保传入参数类型是正确匹配的,或者如果类型不正完全匹配,但是可以将其转换为正确类型,并且返回值在目标表达式里匹配正确类型,或者可以转换为目标类型,内联函数就会直接替换函数调用,这就消除了函数调用的开销。假如内联函数是成员函数,对象this指针也会被放入合适位置。
类型检查和类型转换、包括在合适位置放入对象this指针这些都是预处理器不能完成的。
内联函数不能递归。
但是C++内联编译会有一些限制,以下情况编译器可能考虑不会将函数进行内联编译:
■不能存在任何形式的循环语句
■不能存在过多的条件判断语句
■函数体不能过于庞大
■不能对函数进行取址操作
内联仅仅只是给编译器一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联编译。一个好的编译器将会内联小的、简单的函数。
内联函数的好处
1.有宏函数的效率,没有宏函数的缺点
2.类的成员函数默认加上inline
内联函数的缺点
1.占用更多内存,如果程序在10个不同的地方调用同一个内联函数,该程序将包含函数代码的10个副本
在普通函数前面加上inline是申请成为内联函数
#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS 1
//程序清单8.1
//2023年1月8日11:19:40
//inline.cpp--使用内联函数
#include <iostream>
using namespace std;
//内联函数定义
inline double square(double x) { return x * x; }
int main()
{
double a, b;
double c = 13.0;
a = square(5.0);
b = square(4.5 + 7.5);//可以传递表达式
cout << "a = " << a << ", b = " << b << "\n";
cout << "c = " << c;
cout << ", c squared = " << square(c++) << "\n";
cout << "Now c = " << c << "\n";
system("pause");
return EXIT_SUCCESS;
}
编译器的处理办法
编译器在编译阶段完成对 inline 函数的处理,即对 inline 函数的调用替换为函数的本体。但 inline 关键字对编译器只是一种建议,编译器可以这样去做,也可以不去做。从逻辑上来说,编译器对 inline 函数的处理步骤一般如下:
(1)将 inline 函数体复制到inline函数调用处;
(2)为所用 inline 函数中的局部变量分配内存空间;
(3)将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
(4)如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用GOTO)。
比如如下代码:
// 求 0-9 的平方
inline int inlineFunc(int num)
{
if (num > 9 || num < 0) return -1;
return num * num;
}
int main(int argc, char* argv[])
{
int a = 8;
int res = inlineFunc(a);
cout << "res:" << res << endl;
}
inline 之后的 main 函数代码类似于如下形式:
int main(int argc,char* argv[])
{
int a = 8;
{
int _temp_b=8;
int _temp;
if (_temp_q >9||_temp_q<0) _temp = -1;
else _temp =_temp*_temp;
b = _temp;
}
}
经过以上处理,可消除所有与调用相关的痕迹以及性能的损失。inline 通过消除调用开销来提升性能。
3.4内联函数优缺点
inline 函数的优点总结如下:
(1)内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
(2)内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
例如宏函数和内联函数:
// 宏函数
#define MAX(a,b) ((a)>(b)?(a):(b))
// 内联函数
inline int MAX(int a,int b)
{
return a>b?a:b;
}
使用宏函数时,其书写语法也较为苛刻,如果对宏函数出现如下错误的调用,MAX(a,"Hello");
宏函数会错误地比较int和字符串,没有参数类型检查,但是使用内联函数的时候,会出现类型不匹配的编译错误。
(3)在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
(4)内联函数在运行时可调试,而宏定义不可以。
inline 函数的缺点总结如下:
(1)代码膨胀。
inline 函数带来的运行效率是典型的以空间换时间的做法。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
(2)inline 函数无法随着函数库升级而升级。
如果f是函数库中的一个inline函数,使用它的用户会将f函数实体编译到他们的程序中。一旦函数库实现者改变f,所有用到f的程序都必须重新编译。如果f是non-inline的,用户程序只需重新连接即可。如果函数库采用的是动态连接,那这一升级的f函数可以不知不觉的被程序使用。
(3)是否内联,程序员不可控。
inline 函数只是对编译器的建议,是否对函数内联,决定权在于编译器。编译器认为调用某函数的开销相对该函数本身的开销而言微不足道或者不足以为之承担代码膨胀的后果则没必要内联该函数,若函数出现递归,有些编译器则不支持将其内联。
3.5注意事项
了解了内联函数的优缺点,在使用内联函数时,我们也要注意以下几个事项和建议。
(1)使用函数指针调用内联函数将会导致内联失败。
也就是说,如果使用函数指针来调用内联函数,那么就需要获取inline函数的地址。如果要取得一个inline函数的地址,编译器就必须为此函数产生一个函数实体,那么就内联失败。
(2)如果函数体代码过长或者有多重循环语句,if 或 witch 分支语句或递归时,不宜用内联。
(3)类的 constructors、destructors 和虚函数往往不是 inline 函数的最佳选择。
类的构造函数(constructors)可能需要调用父类的构造函数,析构函数同样可能需要调用父类的析构函数,二者背后隐藏着大量的代码,不适合作为inline函数。虚函数(destructors)往往是运行时确定的,而inline是在编译时进行的,所以内联虚函数往往无效。如果直接用类的对象来使用虚函数,那么对有的编译器而言,也可起到优化作用。
(4)至于内联函数是定义在头文件还是源文件的建议。
内联展开是在编译时进行的,只有链接的时候源文件之间才有关系。所以内联要想跨源文件必须把实现写在头文件里。如果一个 inline 函数会在多个源文件中被用到,那么必须把它定义在头文件中。
// base.h
class Base { protected:void fun(); };
// base.cpp
#include base.h
inline void Base::fun() {}
// derived.h
#include base.h
class Derived : public Base { public:void g(); };
// derived.cpp
void Derived::g() { fun(); } // VC2010: error LNK2019: unresolved external symbol
上面这种错误,就是因为内联函数 fun() 定义在编译单元 base.cpp 中,那么其他编译单元中调用fun()的地方将无法解析该符号,因为在编译单元 base.cpp 生成目标文件 base.obj 后,内联函数fun()已经被替换掉,编译器不会为 fun() 生成函数实体,链接器自然无法解析。所以如果一个 inline 函数会在多个源文件中被用到,那么必须把它定义在头文件中。
这里有个问题,当在头文件中定义内联函数,那么被多个源文件包含时,如果编译器因为 inline 函数不适合被内联时,拒绝将inline函数进行内联处理,那么多个源文件在编译生成目标文件后都将各自保留一份inline函数的实体,这个时候程序在链接阶段会出现重定义错误吗?答案是不会,原因是,链接器在链接的过程中,会删除多余的 inline 函数实体,只保留一份,所以不会报重定义错误,因此我们不需要使用 static 关键字去多余地修饰 inline 函数,即不必像下面这样。
// test.h
static inline int max(int a,int b)
{
return a>b?a:b;
}
(5)能否强制编译器进行内联操作?
也有人可能会觉得能否强制编译器进行函数内联,而不是建议编译器进行内联呢?很不幸的是目前还不能强制编译器进行函数内联,如果使用的是 MS VC++, 注意 __forceinline 如同 inine 一样,也是一个用词不当的表现,它只是对编译器的建议比inline更加强烈,并不能强制编译器进行 inline 操作。
(6)如何查看函数是否被内联处理了?
在 VS2017 中查看预处理后的.i文件,发现inline函数的内联处理不是在预处理阶段,而是在编译阶段。将源文件编译为汇编代码,或者将可执行文件反汇编生成汇编代码,在汇编代码中查看inline函数被调用处是否出现汇编的call指令,如果没有则说明inline函数在被调用处进行了函数体的替换操作,即内联处理。具体可以参考内联函数到底有没有被嵌入到调用处呢?。
(7)C++ 类成员函数定义在类体内为什么不会报重定义错误?
类成员函数定义在类体内,并随着类的定义放在头文件中,当被不同的源文件包含,那么每个源文件都应该包含了类成员函数的实体,为何在链接的过程中不会报函数的重定义错误呢?
原因是在类里定义时,这种函数会被编译器编译成内联函数,在类外定义的函数则不会。内联函数的好处是加快程序的运行速度,缺点是会增加程序的尺寸。比较推荐的写法是把一个经常要用的而且实现起来比较简单的小型函数放到类里去定义,大型函数最好还是放到类外定义。
可能存在疑问,类体内的成员函数被编译器内联处理,但并不是所有的成员函数都会被内联处理,比如包含递归的成员函数。但是实际测试,将包含递归的成员函数定义在类体内,被不同的源文件包含并不会报重定义错误,为什么会这样呢?请保持着疑问与好奇心,请继续往下看。
如果编译器发现被定义在类体内的成员函数无法被内联处理,那么在程序的链接过程中也不会出现函数重定义的错误。其原因是什么呢?其实很简单,类体内定义的成员函数即使不被内联处理,在链接时,链接器会对重复的成员函数实体进行冗余优化,只保留一份函数实体,也就不会出现函数重定义的错误了。
除了 inline 函数,C++编译器在很多时候都会产生重复的代码,比如模板(Templates)、虚函数表(Virtual Function Table)、类的默认成员函数(构造函数、析构函数和赋值运算符)等。以函数模板为例,在多个源文件中生成相同的实例,链接时不会出现函数重定义的错误,实际上是一个道理,因为链接器会对重复代码进行删除,只保留一份函数实体。
3.6小结
可以将内联理解为 C++ 中对于函数专有的宏,对于 C 的函数宏的一种改进。对于常量宏,C++ 提供 const 替代;而对于函数宏,C++ 提供的方案则是 inline。C++ 的内联机制,既具备宏代码的效率,又增加了安全性,还可以自由操作类的数据成员,算是一个比较完美的解决方案。
————————————————
版权声明:本文为CSDN博主「恋喵大鲤鱼」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/K346K346/article/details/52065524
参考资料
参考资料来源于黑马程序员等