9.4 内联函数和编译器
为了理解内联何时有效,应该先理解当编译器遇到一个内联函数时将做什么。对于任何函数,编译器在它的符号表里放入函数类型(即包括名字和参数类型的函数原型及函数的返回类型)。另外,当编译器看到内联函数和对内联函数体的进行分析没有发现错误时,就将对应于函数体的代码也放入符号表。代码是以源程序形式存放还是以编译过的汇编指令形式存放取决于编译器。
当调用一个内联函数时,编译器首先确保调用正确,即所有的参数类型必须满足:要么与函数参数表中的参数类型一样,要么编译器能够将其转换为正确类型,并且返回值在目标表达式里应该是正确类型或可改变为正确类型。当然,编译器为任何类型函数都是这样做的,并且这是与预处理器显著的不同之处,因为预处理器不能检查类型和进行转换。
假如所有的函数类型信息符合调用的上下文的话,内联函数代码就会直接替换函数调用,这消除了调用的开销,也考虑了编译器的进一步优化。假如内联函数也是成员函数,对象的地址(this)就会被放入合适的地方,这个动作当然也是预处理器不能完成的。
9.4.1 限制
有两种编译器不能执行内联的情况。在这些情况下,它就像对非内联函数一样,根据内联函数定义和为函数建立存储空间,简单地将其转换为函数的普通形式。假如它必须在多重编译单元里做这些(通常将产生一个多定义错误),连接器就会被告知忽略多重定义。
假如函数太复杂,编译器将不能执行内联。这取决于特定的编译器,但对于大多数编译器这时都会放弃内联方式,这时内联将可能不能提高任何效率。一般地,任何种类的循环都被认为太复杂而不扩展为内联函数。循环在函数里可能比调用要花费更多的时间。假如函数仅由简单语句组成,编译器可能没有任何内联的麻烦,但假如函数有许多语句,调用函数的开销将比执行函数体的开销少多了。记住,每次调用一个大的内联函数,整个函数体就被插入在函数调用的地方,所以很容易使代码膨胀,而程序性能上没有任何显著的改进。(在本书中的一些例子中使用的内联函数可能超过一定合理的内联尺寸。)
假如要显式地或隐式地取函数地址,编译器也不能执行内联。因为这时编译器必须为函数代码分配内存从而产生一个函数的地址。但当地址不需要时,编译器仍将可能内联代码。
内联仅是编译器的一个建议,编译器不会被强迫内联任何代码。一个好的编译器将会内联小的、简单的函数,同时明智地忽略那些太复杂的内联。这将给我们想要的结果—具有宏效率的函数调用的真正的语义学。