8.3 内联函数和编译器

为了理解内联何时有效,应该先理解编译器遇到一个内联函数时将做什么。对于任何函数,编译器在它的符号表里放入函数类型(即包括名字和参数类型的函数原型及函数的返回类型)。编译器看到内联函数和内联函数的分析没有错误时,函数的代码也被放入符号表。代码是以源程序形式存放还是以编译过的汇编指令形式存放取决于编译器。调用一个内联函数时,编译器首先确保调用正确,即所有的参数类型必须是正确类型或编译器必须能够将类型转换为正确类型,并且返回值在目标表达式里应该是正确类型或可改变为正确类型。当然,编译器对任何类型函数都是这样做的,这与预处理器显著不同,因为预处理器不能检查类型和进行转换。假如所有的函数类型信息符合调用的上下文的话,内联函数代码就会直接替换函数调用,消除了调用的开销。假如内联函数也是成员函数,对象的地址(this)就会被放入合适的地方,这当然也是预处理器不能执行的。

如果你对 C++ 中的内联函数有更多兴趣,可以参考这篇详解的文章 C++inline内联函数详解,里面有更详细的解释和例子。还有一本关于内联函数的全面笔记 内联函数笔记,推荐你看看,里面的内容非常丰富。

8.3.1 局限性

这儿有两种编译器不能处理内联的情况。在这些情况下,它就像对非内联函数一样,通过定义内联函数和为函数建立存贮空间,简单地将其转换为函数的普通形式。假如它必须在多编译单元里做这些(通常将产生一个多定义错误),连接器就会被告知忽略多重定义。假如函数太复杂,编译器将不能执行内联。这取决于特定编译器,但大多数编译器这时都会放弃内联方式,因为这时内联将可能不为我们提供任何效率。一般地,任何种类的循环都被认为太复杂而不扩展为内联函数。循环在函数里可能比调用要花费更多的时间。假如函数仅有一条简单语句,编译器可能没有任何内联的麻烦,但假如有许多语句,调用函数的开销将比执行函数体的开销少多了。记住,每次调用一个大的内联函数,整个函数体就被插入在函数调用的地方,所以没有任何引人注目的执行上的改进就使代码膨胀。本书的一些例子可能超过了一定的合理内联尺寸。假如我们要显式或隐含地取函数地址,编译器也不能执行内联。因为这时编译器必须为函数代码分配内存从而为我们产生一个函数的地址。但当地址不需要时,编译器仍可能内联代码。我们必须理解内联仅是编译器的一个建议,编译器不强迫内联任何代码。一个好的编译器将会内联小的、简单的函数,同时明智地忽略那些太复杂的内联。这将给我们想要的结果—具有宏效率的函数调用。

对于进一步了解内联函数在实际中的应用,看看这篇文章 内联函数与普通函数的区别,它详细讲解了两者之间的不同之处,并附带了不少实用的代码示例。

8.3.2 赋值顺序

假如我们想象编译器对执行内联做了些什么时,我们可能糊里糊涂地认为存在着比事实上更多的限制。特别是,假如一个内联函数对于一个还没有在类里声明的函数进行向前引用,编译器就可能不能处理它。

编译器的这些复杂操作看起来是不是有点令人眼花缭乱?如果你觉得有点复杂,可以看看这篇更基础的解释 C++编程中对内联函数的理解和使用,它用更通俗的语言解答了这些疑问。