
内联函数——多么美妙的想法它们长得像函数行为像函数比宏强太多了参见条款2而且调用它们还无需承担函数调用的开销。你还能奢求什么呢实际上你能得到的好处还不止这些因为免除函数调用开销只是故事的一部分。编译器优化通常针对不含函数调用的代码段进行所以当你内联一个函数时编译器就有机会结合调用上下文对函数体做针对性的优化。大多数编译器对“非内联”的函数调用是不会做这类优化的。然而编程跟生活一样没有免费的午餐内联函数也不例外。内联的基本思路是用函数体代码替换每一次函数调用用不着统计学博士也能明白这很可能会增加目标代码的体积。在内存有限的机器上过度热衷内联可能导致程序体积超过可用空间。即便有虚拟内存内联带来的代码膨胀也可能引发额外的分页操作、降低指令缓存命中率从而拖累性能。反过来如果内联函数体非常短小为函数体生成的代码可能比一次函数调用本身还小。如果是这种情况内联反而有可能让目标代码变得更小指令缓存命中率更高请记住inline 只是对编译器的一个请求而非命令。这个请求可以隐式提出也可以显式提出。隐式方式是在类定义内部定义函数这类函数通常是成员函数但条款46指出友元函数也可以定义在类内部。如果这样做了它们也同样会被隐式声明为内联。显式声明内联函数的方式是在函数定义前加上 inline 关键字。例如标准库中的 max 模板来自algorithm通常就是这样实现的max 是个模板这一点引出了一个常见现象内联函数和模板通常都定义在头文件中。于是有些程序员就得出结论认为函数模板必须声明为 inline。这个结论既不正确还可能带来麻烦所以我们有必要稍作探讨。内联函数通常必须放在头文件中因为大多数构建环境是在编译期完成内联展开的——编译器要把函数调用替换成函数体就得先知道函数长什么样当然有些构建环境可以在链接期做内联极少数——比如基于 .NET 公共语言基础结构CLI的托管环境——甚至能在运行时内联但这些都是例外情况大多数 C 程序中的内联都是在编译期进行的。模板通常也放在头文件中因为编译器在实例化模板时需要知道它的定义同样这并非放之四海皆准——有些构建环境在链接期才做模板实例化但编译期实例化更为普遍。模板实例化与内联是两码事。如果你在写一个模板并且认为从该模板实例化出的所有函数都应该内联那就把模板声明为 inline——上面 std::max 的实现正是这么做的。但如果你写的模板函数没什么理由要内联就不要声明该模板为 inline无论是显式还是隐式。内联有代价你不该在没有深思熟虑之前就贸然承担它。我们前面提到过内联会导致代码膨胀这对模板作者尤其需要重视——参见条款44但它还有其他代价我们稍后会谈到。在此之前先说完一个事实inline 只是请求编译器可以忽略。大多数编译器拒绝内联它们认为过于复杂的函数比如包含循环或递归的而且除了最微不足道的虚函数调用外几乎所有的虚函数都无法内联。后者并不让人意外——virtual 意味着“等到运行时再决定调用哪个函数”而 inline 意味着“执行之前就把调用点替换成函数体”。如果编译器根本不知道会调用哪个函数它拒绝内联该函数体也情有可原。综合以上种种可以得出一个结论一个内联函数是否真的被内联取决于你使用的构建环境——主要是编译器。幸运的是大多数编译器都有一个诊断级别如果你请求内联的函数最终未被内联它会发出警告参见条款53。有时即使编译器完全愿意内联某个函数它仍然会为该函数生成一个独立的函数体。比如如果你的程序取了某个内联函数的地址编译器通常就必须为它生成一个非内联的函数体——它们怎么给一个不存在的函数提供指针呢再加上编译器通常不会对通过函数指针的调用执行内联这就意味着对内联函数的调用是否会被内联取决于调用的方式即使你从不使用函数指针未被内联的内联函数也可能给你带来麻烦因为索取函数指针的不一定是你——编译器有时也会生成构造函数和析构函数的非内联副本以便在数组对象的构造和析构过程中获取这些函数的指针来使用。事实上构造函数和析构函数往往比乍看之下更不适合内联。比如考虑下面 Derived 类的构造函数这个构造函数看起来是个绝佳的内联候选因为它里面没有代码。但表象会骗人。C 对对象创建和销毁时会发生的事情做了各种保证。比如你使用 new 时动态创建的对象会自动被其构造函数初始化使用 delete 时对应的析构函数会被调用。当你创建一个对象时该对象的每个基类子对象和每个数据成员都会被自动构造销毁对象时则会自动执行相反的过程。如果在构造对象的过程中抛出异常任何已经构造完成的部分都会被自动销毁。在所有这些场景中C 规定了“什么必须发生”但没说“如何发生”。那是编译器实现者的事但有一点很清楚这些事不会自己凭空发生。你的程序里必须有某些代码来完成它们而那些代码——由编译器编写、在编译期间插入到程序中的代码——总得有个去处。有时它们就落到了构造函数和析构函数里所以我们可以想象上面那个看似为空的 Derived 构造函数其实现代码可能等效于下面这样这段代码并不代表真正编译器生成的实现因为实际编译器会以更巧妙的方式处理异常。但它确实准确地反映了 Derived 这个“空”构造函数必须提供的行为。无论编译器的异常实现多么精巧Derived 的构造函数至少还得去调用其数据成员和基类的构造函数而这些调用它们本身也可能被内联或多或少会影响该构造函数作为内联候选的吸引力。同理也适用于 Base 的构造函数所以如果基类构造函数被内联了那么插入到它里面的所有代码也会通过 Derived 构造函数对基类构造函数的调用而一并插入到 Derived 构造函数中。而如果 string 的构造函数恰好也被内联那么 Derived 构造函数就会获得五份该函数的代码副本——对应 Derived 对象中的五个 string继承自基类的两个加上自己声明的三个。现在你应该明白了是否把 Derived 的构造函数声明为内联并不是一个想当然就能拍板的决定。同样的情况也适用于 Derived 的析构函数——它无论如何都必须确保所有由 Derived 构造函数初始化过的对象能被正确地销毁。库的设计者在决定将函数声明为 inline 时必须评估其影响因为一旦库中某个客户端可见的内联函数被改动就无法提供“二进制升级”——换句话说如果库里的 f 是内联函数那么库的客户会把 f 的函数体编译到他们自己的应用程序中。若库的实现者后来决定修改 f所有使用过 f 的客户都必须重新编译。这往往不是人们想要的。反过来如果 f 是非内联函数修改 f 只需客户重新链接即可——这个负担比重新编译轻得多而且如果包含该函数的库是动态链接的甚至可以做到对客户完全透明。就程序开发而言把这些考量都放在心上很重要。但从编码的实用角度来看有一个事实压倒其他一切大多数调试器对内联函数都束手无策。这其实并不令人意外——你怎么在一个不存在的函数里设置断点呢虽然有些构建环境设法支持对内联函数的调试但很多环境在调试构建时干脆直接禁用了内联。由此可以得出一个合理的策略来判断哪些函数应该声明为内联哪些不应该一开始先不要内联任何东西或者至少把内联限制在那些必须内联参见条款46或极其简单的函数上比如第135页的 Person::age。谨慎使用内联既能方便你用调试器也能把内联放在它该在的位置——一种手工施加的优化。别忘了那条经验法则“80-20法则”一个典型程序会把 80% 的执行时间花在仅 20% 的代码上。这条法则很重要因为它提醒你作为软件开发者的目标是找出那 20% 能提升程序整体性能的代码。你可以对内联函数做各种调优但如果没找对目标函数那都是白费力气。切记1.大多数内联应仅限于小型、频繁调用的函数。这样做既方便调试也利于二进制升级还能最大程度避免代码膨胀并最有可能提升程序运行速度。2.不要仅仅因为函数模板出现在头文件中就把它们声明为 inline。