C++和C99均支持内联函数inline,按这个名称的意思应该是“在字里行间展开”,内联函数会在它被调用的位置展开 。
讨论之前,我们先来看一个故事:实验室搬至六楼,小件器件特别多,一同学正一件一件地往六楼搬,老师借给他一个大袋子,这同学一次就把这些小物件搬上了六楼,虽然增加了一个大袋子的重量,却省去了上下六楼100多趟之苦。这个大袋子就充当了内联函数的功能。
我们都知道,一个函数的调用要付出时间上的代价,其大致过程为:
\1. 保护现场,就是先将主调函数里的函数调用返回后要执行的指令的地址压入栈中保存;
\2. 把被调函数的形参和auto存储类型的变量压入栈区保存,这一步压入的所有变量所占有的存储我们称之为被调函数的数据现场;
\3. 执行完被调函数之后,把被调函数数据现场释放(出栈);
\4. 把第1步压入的指令地址出栈,即恢复现场,然后找到这个地址继续执行。
要是一个函数被调用了许多次,那么编译系统需要来来回回的往返许多趟,存在栈内存创建和释放的开销,于是C++(C99)的编译系统提供了一个大袋子。在被调函数的定义前加一个标志(inline)告诉编译系统,编译系统看到这个标志后,实际编译出的可执行程序,就如同用函数体合理地置换了函数被调用处一样,我们称之为内联机制。
内联机制可以消除函数调用和返回带来的开销(寄存器存储和恢复),另外,编译器也会把调用函数的代码和函数本身放在一起进行优化,那么,是不是干脆把所有的函数都定义称内联函数呢,天底下没有免费的午餐,在计算机科学中,空间和时间永远是个矛盾体。内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,需要占用更多的内存空间或者更多的指令缓存,最终生成的可执行程序的体积会有膨胀,这是典型的以空间换时间的做法。
这是对于函数体比较短小的情况,但如果函数体比较大,执行函数体内代码的时间,相比于函数调用的开销比较大,那么效率的收获会很少,而且,每一处内联函数的调用都要拷贝一份代码,将使程序的总代码量增大,消耗更多的内存空间,这样就不值得了。
一般来说,具有以下特征的函数适宜定义成内联函数:
\1. 函数体内的语句数量较少,如果函数体内代码比较长,使用内联将导致内存消耗代价较高;
\2. 函数体内没有循环,递归等。出现循环,执行函数体内代码的时间要比函数调用的开销大;
\3. 在满足上面两个前提下,函数实际调用次数却很多的情况。就可以考虑将函数定义为内联来提高效率。
那么内联函数究竟是如何工作的呢?
当我们定义了一个函数之后,编译器会将其编译成一个指令集合。这个指令集合在程序运行的时候会出现在内存的代码区里,并且在调用此函数时程序执行的地址会跳转到这个指令集合的入口地址,当指令集合执行完后,再跳回到主调函数。换句话说,任何时候内存中只有一个指令集,如果该函数被调用10次,则运行时就会跳转到同一入口地址10次。
如果定义为inline函数,编译器并不创建真实函数,内联函数不仅同普通函数一样经过检查后保存函数名称、参数类型和返回值类型,还会把内联函数的本体也一并存入符号表中,在之后的编译过程中一旦遇到该函数被调用时会首先检查调用是否合法,然后编译器会将inline函数的指令集合(函数代码)复制嵌入到主调函数中的调用位置,内联函数的代码就会直接替换函数调用,这样就不需要函数调用的跳转开销了。如果函数被调用了10次,就相当于内存中就包含10个相同指令集合的拷贝,没有一次调用。
了解了内联函数是怎么工作的,那么内联机制的优劣就好理解了。需要清楚的是,我们定义为inline函数只是建议编译器进行内联,而不是命令编译器进行内联,所以最后是不是内联函数取决于编译器。还有关键字inline必须与函数定义放在一起才能使函数成为内联(最后由编译器决定),仅放在函数声明前面不起作用。因为inline是在编译时展开,必须有实体,在编译阶段,编译器看到inline标志就会根据该函数体情况去判断是否应该将该函数体定义为内联。没看到本体怎么判断勒。
inline是一种“用于实现的关键字”,内联函数不能带有virtual关键字。常把inline函数定义为static类型,可以在不同文件中定义同名文件,而不必担心命名冲突,所有未加static前缀的函数都具有全局可见性,其他源文件也能访问。