天天看点

C++内联函数机制全面解析内联函数机制的引入inline关键字内联函数什么时候不展开宏定义与内联函数的区别内联函数与普通函数的区别有哪些

内联函数机制的引入

        内联机制被引入C++作为对宏(Macro)机制的改进和补充(不是取代)。内联函数的参数传递机制与普通函数相同。但是编译器会在每处调用内联函数的地方将内联函数的内容展开。这样既避免了函数调用的开销又没有宏定义机制的缺陷。由此可见,内联函数机制的引入与宏定义有很大关系,因此,有必要先了解下宏定义有哪些缺陷。

        1. 由于宏定义都是直接嵌入代码中的,所以代码可能相对多一点;

        2. 嵌套定义过多可能会影响程序的可读性,而且很容易出错;

        3. 对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患,预编译语句仅仅是简单的值代替,缺乏类型的检测机制。这样预处理语句就不能享受C++严格的类型检查的好处,从而可能成为引发一系列错误的隐患。

        最后引用《C陷进与缺陷》的一句话,对其进行总结:“宏并不是函数,宏并不是语句,宏并不是类型定义 ”。

inline关键字

        但是程序代码中的关键字"inline"只是对编译器的建议:被"inline"修饰的函数不一定被内联(但是无"inline"修饰的函数一定不是)。

        许多书上都会提到这是因为编译器比绝大多数程序员都更清楚函数调用的开销有多大,所以如果编译器认为调用某函数的开销相对该函数本身的开销而言微不足道或者不足以为之承担代码膨胀的后果则没必要内联该函数。这当然有一定道理,但是按照C、C++一脉相承的赋予程序员充分自由与决定权的风格来看,理由还不够充分。我猜想最主要的原因是为了避免编译器陷入无穷递归。如果内联函数之间存在递归调用则可能导致编译器展开内联函数时陷入无穷递归。有时候函数的递归调用十分隐蔽,程序员并不容易发现,所以简单起见,将内联函数展开与否的决定权交给编译器。

        开发人员有两种方式告诉编译器需要内联哪些函数,一种是在类的定义体外,一种是在类的定义体内。

        (2)当在类的定义体内且声明该成员函数时,同时提供成员函数的实现体。此时“inline”关键字不是必需的。

        因为C++是以“编译单元”为单位编译的,而一个编译单元往往大致等于一个“.cpp”文件。在实际编译前,预处理器会将“#include”的各个头文件的内容(可能会有递归头文件展开)完整地复制到cpp文件对应位置处(另外还会进行宏展开等操作)。预处理器处理后,编译真正开始。一旦C++编译器开始编译,它不会意识到其他cpp文件的存在。因此并不会参考其他cpp文件的内容信息。联想到内联的工作是由编译器完成的,且内联的意思是将被调用的内联函数的函数体代码直接代替对该内联函数的调用。这也就意味着,在编译某个编译单元时,如果该编译单元会调用到某个内联函数,那么该内联函数的函数定义(即函数体)必须也包含在该编译单元内。因为编译器使用内联函数体代码替代内联函数调用时,必须知道该内联函数的函数体代码,而且不能通过参考其他编译单元信息来获取这一信息。

        如果有多个编译单元会调用到同一个内联函数,C++规范要求在这多个编译单元中该内联函数的定义必须完全一致的。考虑到代码的维护性,最好将内联函数的定义放在一个头文件中,用到该内联函数的各个编译单元只需#include(包含)该头文件即可,进一步考虑,如果该内联函数是一个类的成员函数,这个头文件正好可以是该成员函数所属类的声明所在的头文件。

内联函数什么时候不展开

        在内联函数内不允许用循环语句和开关语句(开关语句即switch语句)。如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码,递归函数(自己调用自己的函数)是不能被用来做内联函数的。内联函数只适合于只有1~5行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,也没有必要用内联函数实现。

        另一种不被内联的情况是使用函数指针来调用内联函数(解释见引申部分)。

        对于C++中内联机制的一个常见误解是:关键字"inline"只是对编译器的建议,如果编译器发现指定的函数不适合内联就不会内联;所以即使内联使用得不恰当也不会有任何副作用。这句话只对了一半,内联使用不恰当是会有副作用的:会带来代码膨胀,还有可能引入难以发现的程序臭虫。

        根据规范,当编译器认为希望被内联的函数不适合内联的时候,编译器可以不内联该函数。但是不内联该函数不代表该函数就是一个普通函数了,从编译器的实际实现上来讲,内联失败的函数与普通函数是有区别的:

        (1)普通函数在编译时被单独编译一个对象,包含在相应的目标文件中。目标文件链接时,函数调用被链接到该对象上。

        (2)若一个函数被声明成内联函数,编译器即使遇到该函数的声明也不会为该函数编译出一个对象,因为内联函数是在用到的地方展开的。可是若在调用该内联函数的地方发现该内联函数的不适合展开时怎么办?一种选择是在调用该内联函数的目标文件中为该内联函数编译一个对象。这么做的直接后果是:若在多个文件调用了内联失败的函数,其中每个文件对应的目标文件中都会包含一份该内联函数的目标代码。

        如果编译器真的选择了上面的做法对待内联失败的函数,那么最好的情况是:没吃到羊肉,反惹了一身骚。即内联的好处没享受到,缺点却承担了:目标代码的体积膨胀得与成功内联的目标代码一样,但目标代码的效率确和没内联一样。

        更糟的是由于存在多份函数目标代码带来一些程序臭虫。最明显的例子是:内联失败的函数内的静态变量实际上就不在只有一份,而是有若干份。这显然是个错误,但是如果不了解内幕就很难找到原因。

宏定义与内联函数的区别

        从内联即函数体代码替代对该函数的调用这一本质看,它与C语言中的宏极其相似,但是它们之间有本质区别。宏代码本身不是函数,但使用起来却像函数,预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高了速度。内联函数是代码被插入到调用者代码处的函数。对C++而言,内联函数的作用也不是万能的,它的使用是有所限制的,它只适合函数体内代码简单的函数使用,不能包含复杂的结构控制语句(如switch、while),并且内联函数本身不能直接调用递归函数。

        两者的区别主要表现在以下几个方面:第一,宏定义在预处理阶段进行代码替换,而内联函数是在编译阶段插入代码;第二,宏定义没有类型检查,而内联函数有类型检查,这对于写出正确且鲁棒的程序是一个很大的优势;最后,宏肯定会被展开,而用inline关键字修饰的函数不一定会被内联展开。

内联函数与普通函数的区别有哪些

        内联函数的参数传递机制与普通函数相同,但是编译器会在每处调用内联函数的地方将内联函数的内容展开,这样既避免了函数调用的开销又没有宏机制的缺陷。

        内联函数和普通函数的最大区别在于其内部的实现,普通函数在被调用时,系统首先要跳跃到函数的入口地址,执行函数体,执行完毕后,再返回到函数调用的地方,函数始终只有一个复制;而内联函数则不需要进行一个寻址过程,当执行内联函数时,此函数展开,如果在N处调用了此内联函数,则此函数就会有N个代码段的复制。

        空间和时间比较,假设调用一个函数之前的准备工作和之后的善后工作的指令所需空间大小为SS,执行这些代码所需时间为TS。

        (1)空间。如果一个函数的函数体代码大小为AS,在程序中被调用N次,不采用内联的情况下,空间开销为:SS*N+AS。采用内联:AS*N。因为N一般很大,所以它们之间的比较就是SS跟AS的比较,得出的结论是:如果SS小于AS,不采用内联,空间开销更少。如果AS小于SS,则采用内联,空间开销更少。

        (2)时间。内联之后每次调用不再需要做函数调用的准备和善后工作;内联之后编译器获得更多的代码信息,看到的是调用函数与被调用函数连成的一大块代码,此时编译器对代码的优化可以做得更好。还有一个很重要的因素,即内联后调用函数体内需要执行的代码是相邻的,其执行的代码都在同一个页面或连续的页面中。如果没有内联,执行到被调用函数时,需要跳到包含被调用函数的内存页面中执行,而被调用函数所属的页面极有可能当时不在物理内存中。这意味着,内联后可以降低“缺页”的几率,“缺页”次数的减少带来的效果远好于代码量的减少。另外即使被调用函数所在的页面可能正好在物理内存中,但是因为与调用函数在空间上相隔甚远,所以可能会引起“Cache miss”,从而降低执行速度。因此总的来说,内联后程序的执行时间会比没有内联要少,即程序速度更快。不过,如果内联的函数非常大的话,正如前面提到的,当AS远大于SS,且N很大时,会使最终程序的代码量增多,代码量多意味着用来存放代码的内存页面增多,“缺页”也会相应增加,速度反而下降,所以很大的函数不适合内联。这也是为什么很多编译器对于函数体代码很多的函数,会拒绝对其进行内联的请求。即忽略"inline"关键字,而对如同普通函数那样编译。

        最后顺带提及,一个程序的唯一入口main()函数肯定不会被内联化。另外编译器合成的默认构造函数、拷贝构造函数、析构函数以及赋值运算符一般都被内联化。

继续阅读