详细介绍内联函数的使用

发表于:2011-7-01 09:42

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:未知    来源:51Testing软件测试网采编

分享:

  乍看上去,将这个构造函数进行内联再适合不过了,因为它不包含任何代码。其实你的眼睛欺骗了你。

  C++ 对于在创建和销毁对象的过程中发生的事件进行了多方面的保证。比如,当你使用 new 时,你动态创建的对象的构造函数就会自动将其初始化;当你使用 delete 时,将调用相关的析构函数。当你创建一个对象时。每个基类和该对象中的每个数据成员将自动得到构造,在销毁这个对象时,针对两者的析构过程将会自动进行。如果在对象的构造过程中有异常抛出,那么对象中已经得到构造的部分将统统被自动销毁。

  在所有这些场景中, C++ 告诉你什么一定会发生,但它没有说明如何发生。这一点取决于编译器的实现者,但是必须要清楚的一点是,这些事情并不是自发的。你必须要在程序中添加一些代码来实现它们。这些代码一定存在于某处,它们由编译器代劳,用于在编译过程中插入你的程序中。一些时候它们就存在于构造函数和析构函数中,所以,对于上文中 Derived 的空构造函数,我们可以将具体实现中生成的代码等价看作:

  1. Derived::Derived() // Derived 空构造函数的抽象实现   
  2. {   
  3. Base::Base(); // 初始化 Base 部分   
  4. try { dm1.std::string::string(); } // 尝试构造 dm1   
  5. catch (...) { // 如果抛出异常 ,   
  6. Base::~Base(); // 销毁基类部分 ,   
  7. throw// 并且传播该异常   
  8. }   
  9.  
  10. try { dm2.std::string::string(); } // 尝试构造 dm2   
  11. catch(...) { // 如果抛出异常 ,   
  12. dm1.std::string::~string(); // 销毁 dm1,   
  13. Base::~Base(); // 销毁基类部分 ,   
  14. throw// 并且传播该异常   
  15. }   
  16. try { dm3.std::string::string(); } // 尝试构造 dm3   
  17. catch(...) { // 如果抛出异常 ,   
  18. dm2.std::string::~string(); // 销毁 dm2,   
  19. dm1.std::string::~string(); // 销毁 dm1,   
  20. Base::~Base(); // 销毁基类部分 ,   
  21. throw// 并且传播该异常   
  22. }   
  23. }

  这段代码并不能完全真实反映出编译器所做的事情,因为真实的编译器采用的做法更加复杂。然而,上面的代码可以较为精确地反映出 Derived 的“空”构造函数必须要提供的内容。无论编译器处理异常的实现方式多么复杂, Derived 的构造函数必须至少为其数据成员和基类调用构造函数,这些调用(可能就是内联的)会使 Derived 显得不那么适合进行内联。

  这 一推理过程对于 Base 的构造函数同样适用,因此如果将 Base 内联,所有添加进其中的代码同样也会添加进 Derived 的构造函数中(通过 Derived 构造函数调用 Base 构造函数的过程)。同时,如果 string 的构造函数恰巧被内联了,那么 Derived 的构造函数将为其复制出五份副本,分别对应 Derived 对象中包含的五个字符串(两个继承而来,另外三个系对象本身包括)。

  现在,“Derived 的构造函数是否应该内联不是一个纯机械化问题”就很容易理解了。对于 Derived 的析构函数也一样,你必须亲自关注 Derived 的构造函数初始化的对象是否全部恰当的得到销毁,这一点机器无法代替。

  库设计者必须估算出将函数内联所带来的影响,因为你根本无法为库中客户端程序员可见的内联函数提供底层的升级。换句话说,如果 f 是库中的一个内联函数,那么库的客户端程序员就会将 f 的函数体编译进他们的程序中。随后,如果一个库实现者修改了 f 的内容,那么所有曾经使用过 f 的客户端程序员必须要重新编译他们的代码。这一点是我们所不希望看到的。

  另一个角度讲,如果 f 不是内联函数,那么修改 f 只需要客户端程序员重新连接一下就可以了。这样要比重新编译减少很多繁杂的工作,并且,如果库中需要使用的函数是动态链接的,那么它对于客户端程序员就是完全透明的。

  我们的目标是开发优质的程序,因此要将这些重要问题牢记在心。但是以编写代码实际操作的角度来说,这一个事实将淹没一切:大多数调试人员面对内联函数时会遇到麻烦。这并不会令人意外,因为你无法为一个尚不存在的函数设定一个跟踪点。一些构建环境试图支持内联函数的调试,但是几乎都失败了,大多数环境都是在调试过程中直接禁止内联。

  对于“哪个函数应该声明为 inline 而哪些不应该”这一问题,我们可以由上文中引出一个逻辑上的策略。起初,不要内联任何内容,或者仅挑选出那些不得不内联的函数(参见第 46 条)或者那些确实是很细小的程序(比如本节开篇处出现的 Person::age )进行内联。谨慎引入内联,你就为调试工作提供了方便,但是你仍然要为内联摆正位置:它属于手工的优化操作。

  不要忘记 80-20 经验决定主义原则:一个典型的程序将花去 80% 的时间仅仅运行 20% 的代码。这是一个非常重要的原则,因为它时时刻刻提醒我们,软件开发者的目标是:找出你的代码中 20% 的这部分进行优化,从而从整体上提高程序的性能。你可以花费很长的时间进行内联、修改函数等等,但如果你没有锁定正确的目标,那么你做再多的努力也是徒劳。

  结论:

  仅仅对小型的、调用频率高的程序进行内联。这将简化你的调试操作,为底层更新提供方便,降低潜在的代码膨胀发生的可能,并且可以让程序获得更高的速度。不要将模板声明为 inline 的,因为它们一般在头文件中出现。

  希望对你有帮助。

44/4<1234
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号