读书笔记 effective c++ Item 7 在多态基类中将析构函数声明为虚析构函数
1. 继承体系中关于对象释放遇到的问题描述
1.1 手动释放
关于时间记录有很多种方法,因此为不同的计时方法创建一个TimeKeeper基类和一些派生类就再合理不过了:
class TimeKeeper { public: TimeKeeper(); ~TimeKeeper(); ... }; class AtomicClock: public TimeKeeper { ... }; class WaterClock: public TimeKeeper { ... }; class WristWatch: public TimeKeeper { ... };
许多客户端只想访问时间而不想知道关于时间计算的细节,所以可以创建一个工厂方法,这个工厂方法返回一个指向新创建的派生类对象的基类指针,这个指针用来指向一个计时对象:
TimeKeeper* getTimeKeeper(); // returns a pointer to a dynamic- // ally allocated object of a class // derived from TimeKeeper
为了和工厂方法的约定保持一致,getTimeKeeper返回一个堆上的对象,因此为了避免泄露内存和其他资源,每个返回的对象被合理的释放掉(deleted)是很重要的:
TimeKeeper *ptk = getTimeKeeper(); // get dynamically allocated object // from TimeKeeper hierarchy ... // use it delete ptk; // release it to avoid resource leak
Item13中解释到依赖客户执行deletion比较容易出错,在Item18中解释了如何改变工厂函数的接口来预防一般的客户端错误,但是这些关注点在这里都是次要的,因为在这个条款中,我们为上面的代码提出一个更基本的弱点:即使客户端把一切都做对了,根本没有方法去知道程序如何运转。
1.2非虚析构函数引入的问题
问题在于getTimeKeeper返回一个指向派生类对象的指针(AtomicClock),这个对象通过一个基类指针(一个TimeKeeper*指针)来进行释放(delete),基类中(TimeKeeper)有一个非虚析构函数。这是造成灾难的一个因素,因为c++指出:通过一个基类的指针来释放一个派生类的对象,如果基类的析构函数是非虚的,那么结果未定义。在运行时有可能发生以下状况:对象的派生类部分永远不会被释放掉。如果对getTimeKeeper的调用恰巧返回一个指向AtomicClock对象的指针,对象的AtomicClock部分(也就是在AtomicClock类中声明的数据成员)可能不会被释放掉,AtomicClock类的析构函数也不会被执行。然而,基类部分(也就是TimeKeeper部分)是会被释放掉的,这会导致产生一个古怪的“部分被释放的”对象。这是使资源泄露,破坏数据结构和在debugger上花费大把时间的绝佳方法。
2.如何解决问题-声明虚析构函数
消除这个问题很简单:为基类提供一个虚析构函数。这时如果delete一个派生类对象将会做到你想要的。它会释放掉整个对象,包括派生类的所有部分:
class TimeKeeper { public: TimeKeeper(); virtual ~TimeKeeper(); ... }; TimeKeeper *ptk = getTimeKeeper(); ... delete ptk; // now behaves correctly
基类中(TimeKeeper)除了析构函数外一般情况下会包含虚函数,因为虚函数存在的目的是为了函数在派生类中的定制化实现(Item34)。举个例子,TimeKeeper会有一个虚函数,getCurrentTime,这个函数在不同的派生类中会有不同的实现。任何有虚函数的类应该肯定有一个虚析构函数。
3.不要在不当作基类的类中声明虚析构函数
如果类中不包含虚函数,这通常表明它不会被用作基类,如果并没有打算将一个类作为一个基类,将析构函数声明为虚是一个坏的想法。考虑一个表示二维空间的点的类:
class Point { // a 2D point public: Point(int xCoord, int yCoord); ~Point(); private: int x, y; };
如果int占用32Bits,那么一个Point对象可被放入一个64-bit的缓存器中。并且,这个Point对象可以以一个”64-bit quantity”传给用其他语言编写的函数,例如c语言和Fortran。如果将Point的析构函数声明成虚的,状况就会发生变化。
虚函数的实现需要对象带一些信息,根据这些信息在运行时能够决定对象的哪个虚函数会被触发。这些信息表现为一个被叫做vptr(virtual table pointer)指针的形式。我们把指向一个函数指针数组的vptr指针叫做vtbl(virtual table);每个有虚函数的类都有一个关联的vtbl.当虚函数在一个对象上被触发,实际调用的函数是由对象的vtbl中的vptr来决定的,在vtbl中会查找到合适的函数指针。
关于虚函数是如何实现的细节并不重要。重要的是如果Point类中包含一个虚函数,这个类型的对象会在占用空间上有所增加:在32位机器中,空间会从64bits(两个int)增加到96bits;在64位机器中,空间会从64bits增加到128bits,因为64位机器上的指针在空间上占用64bits.Point额外增加了一个vptr而致使内存空间增加50-100%。Point将不能在放进64bits的缓存中。并且,c++中的Point也不再同其他语言(如C语言)中声明的对象有类似的结构了,因为其他语言没有vptr,因此你不再能够向(从)其他语言编写的函数中传进(传出)指针了,除非你对vptr进行明确的补偿,这属于实现细节,代码因此也不能够被移植了。
因此,无缘无故的将所有析构函数声明成虚函数同永远不将其声明为虚函数犯了一样的错误。事实上,许多人将上面的情形其总结如下:在类中声明虚析构函数当且仅当类中至少包含一个虚函数。
4.不要继承析构函数为非虚的类
在虚函数完全缺席的情况下,非虚析构函数的问题同样会导致只释放部分内存的问题。举个例子,标准string类型不包含虚函数,但是一些被误导的程序员有时会将其当作基类:
class SpecialString: public std::string { // bad idea! std::string has a ... // non-virtual destructor };
乍一看这么实现也许无伤大雅,但是如果在一个应用中的某个地方,你以某种方式将指向SpecialString的指针转换成指向string的指针,然后你在string指针上使用delete,你马上会被转到未定义行为的领地:
SpecialString *pss =new SpecialString("Impending Doom"); std::string *ps; ... ps = pss; // SpecialString* ⇒ std::string* ... delete ps; // undefined! In practice, // *ps’s SpecialString resources // will be leaked, because the // SpecialString destructor won’t // be called
同样的分析适用于任何缺少虚析构函数的类,包含所有的STL容器类型(例如 vector,list set,tr1::unordered_map(Item54))。如果你曾经受到诱惑,从一个标准容器类或其他没有虚析构函数的类中继承,你需要抵抗这种诱惑!(不幸的是,c++没有提供不能继承的机制,java中有final类,c#中有sealed类)。
5.纯虚析构函数
偶尔情况下为类提供一个纯虚析构函数是很方便的。有纯虚函数的类是一个抽象类,其不能够被实例化。然而有时候,你想将一个类变成一个抽象类,但是没有任何纯虚函数。该怎么办?因为一个抽象类将来会被用作基类,并且基类应该有一个虚析构函数,同时一个纯虚函数产生一个抽象类,所以解决方案很简单:在你想要其变成抽象的类中声明一个纯虚析构函数。看下面的例子:
class AWOV { // AWOV = “Abstract w/o Virtuals” public: virtual ~AWOV() = ; // declare pure virtual destructor };
这个类有一个纯虚函数,所以它是抽象类。因为它有一个虚析构函数,所以你不必担心因为析构函数出现的问题。这里有个窍门,你必须为纯虚函数提供一份定义:
AWOV::~AWOV() {} // definition of pure virtual dtor
析构函数工作的方法是最底部的派生类先被调用,然后析构函数的每一个基类会被依次调用。编译器会从派生类的析构函数中生成一个对~AWOV的调用,因此你必须确保为这个函数提供一个函数体。如果不提供会有链接错误。
6.其他一些需要注意的地方
为基类提供虚析构函数的法则只适用于多态基类,多态基类也就是将基类设计成允许通过基类接口来操作派生类型的类。TImeKeeper是一个多态基类,因为我们想能够操作AtomicClokc和WaterClock对象,在即使只有TimeKeeper指针指向这些派生类对象的情况下。
并不是所有的基类都被设计成能够使用多态。举个例子,标准string类型还有STL容器类型并没有被设计成基类,更不用说多态了。一些类被设计成当基类使用,但是没有被设计成使用多态。举个例子,Item6中的UnCopyable和来自标准库中的input_iterator_tag(Item47),这样的类没有被设计成通过基类接口操作派生类。因此,也不需要虚析构函数。
读书笔记 effective c++ Item 7 在多态基类中将析构函数声明为虚析构函数的更多相关文章
- 读书笔记 effective c++ Item 36 永远不要重新定义继承而来的非虚函数
1. 为什么不要重新定义继承而来的非虚函数——实际论证 假设我告诉你一个类D public继承类B,在类B中定义了一个public成员函数mf.Mf的参数和返回类型并不重要,所以假设它们都是void. ...
- 读书笔记 effective c++ Item 35 考虑虚函数的替代者
1. 突破思维——不要将思维限定在面向对象方法上 你正在制作一个视频游戏,你正在为游戏中的人物设计一个类继承体系.你的游戏处在农耕时代,人类很容易受伤或者说健康度降低.因此你决定为其提供一个成员函数, ...
- 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数
关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...
- 读书笔记 effective c++ Item 37 永远不要重新定义继承而来的函数默认参数值
从一开始就让我们简化这次的讨论.你有两类你能够继承的函数:虚函数和非虚函数.然而,重新定义一个非虚函数总是错误的(Item 36),所以我们可以安全的把这个条款的讨论限定在继承带默认参数值的虚函数上. ...
- 读书笔记 effective c++ Item 51 实现new和delete的时候要遵守约定
Item 50中解释了在什么情况下你可能想实现自己版本的operator new和operator delete,但是没有解释当你实现的时候需要遵守的约定.遵守这些规则并不是很困难,但是它们其中有一些 ...
- 读书笔记 effective c++ Item 41 理解隐式接口和编译期多态
1. 显示接口和运行时多态 面向对象编程的世界围绕着显式接口和运行时多态.举个例子,考虑下面的类(无意义的类), class Widget { public: Widget(); virtual ~W ...
- 读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库
1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C+ ...
- 读书笔记 effective c++ Item 11 在operator=中处理自我赋值
1.自我赋值是如何发生的 当一个对象委派给自己的时候,自我赋值就会发生: class Widget { ... }; Widget w; ... w = w; // assignment to sel ...
- 读书笔记 effective c++ Item 12 拷贝对象的所有部分
1.默认构造函数介绍 在设计良好的面向对象系统中,会将对象的内部进行封装,只有两个函数可以拷贝对象:这两个函数分别叫做拷贝构造函数和拷贝赋值运算符.我们把这两个函数统一叫做拷贝函数.从Item5中,我 ...
随机推荐
- Python twisted article
学习python twisted 的好文章 An Introduction to Asynchronous Programming and Twisted Reference: http://kron ...
- bzoj1396
传送门:http://www.lydsy.com/JudgeOnline/problem.php?id=1396 题目大意: 题解:后缀自动机,只出现一次,那么就是right值为1,那么对于一段1-- ...
- ora-12154
64位oracle,32位pl/sql pl/sql配置完之后,一直报错: ora-12154 配置环境变量ORACLE_HOME:D:\softInstrall\oracle\product\11. ...
- Python3基础 使用for循环 删除一个列表中的重复项
镇场诗: 诚听如来语,顿舍世间名与利.愿做地藏徒,广演是经阎浮提. 愿尽吾所学,成就一良心博客.愿诸后来人,重现智慧清净体.-------------------------------------- ...
- dotnet调用node.js写的socket服务(websocket/socket/socket.io)
https://github.com/jstott/socketio4net/tree/develop socket.io服务端node.js,.里面有js写的客户端:http://socket.io ...
- Bagging和Boosting 概念及区别
Bagging和Boosting都是将已有的分类或回归算法通过一定方式组合起来,形成一个性能更加强大的分类器,更准确的说这是一种分类算法的组装方法.即将弱分类器组装成强分类器的方法. 首先介绍Boot ...
- spring security 跨域防伪攻击
applicationContext-security.xml中配置 <http use-expressions="true" disable-url-rewriting=& ...
- 记录一次事故——idea,sbt,scala
没事千万不要点idea的update啊,就算它自己弹出来的也不要管哦. 我们自己的IDE在使用过程中总会有各种settting的配置,更新之后这些都没有了,而且自己本地安装的插件也就都没有了,所以更新 ...
- Selenium2(java)页面对象模型(Page Object) 八
在开发一个 Selenium WebDriver 测试,我们可以使用页面对象模型.这个模型可以使测 试脚本有更高的可维护性,减少了重复的代码,把页面抽象出来.对象模型也提供了一个注释,帮助缓存远程,避 ...
- iOS 之 内存管理
凡是alloc copy mutablecopy init 声明的变量,都需要通过手动的方式进行释放,realse. 如果 copy一个对象,则拥有了拷贝的对象,要负责释放. 如果 保持(retain ...