Declare destructors virtual in polymorphic base classes.
 
  • [原理]
C++指出,当derived class对象经由一个由base class类型的指针删除时,如果这个base class 拥有一个non-virtual的析构函数,那个析构的结果将是未定义的。即通常情况下是该对象的base class成分会被析构掉,但是其derived class成分没有被销毁,甚至连derived class的析构函数也不会被调用。
于是形成一个被“局部销毁”的对象,造成资源泄漏。
 
  • [示例]
例如:
class car
{
public:
car();
~car();
...
}; class diesel_car : public car {…};
class solar_car: public car {…};
class electric_car : public car {…};
当客户代码中使用汽车对象时,如果他不关心使用的是具体哪一类汽车这个细节,那么我们可以设计一个工厂函数(或者工厂类)负责创建一个汽车对象,该工厂函数返回一个base class指针或者引用,指向新生成的derived class对象:
car* get_car();

为遵守工厂函数的规矩,返回的对象必须位于heap(否则函数返回的指针在函数返回后将指向一个非法的位置,因为位于stack的对象的生命周期为函数域),因此为了避免内存泄漏,需要客户代码将工厂函数返回的对象适当地delete掉:

car* p_car = get_car();      // 从car继承体系中获得一个动态分配对象
… // 使用这个对象
delete p_car; // 释放这个对象以避免内存泄漏

首先需要说明,上述做法已经存在两个缺陷:

1.依赖客户代码执行delete操作,带有错误倾向,客户可能会忘记做这件事。
2.工厂函数结构应该考虑预防常见的客户代码错误。
 
但是最根本的弱点在于:客户代码根本无法将返回的derived class对象彻底销毁。
 
简单的做法便是:为base class定义一个virtual析构函数。此后删除derived class对象就会销毁这个对象,包括所有的derived class成分。
class car
{
public:
car();
virtual ~car();
...
};
  • [引申1]
当一个类需要被用作多态(Polymorphism)时,就应该为该类声明一个virtual析构函数,即任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
 
但是,如果class没有virtual函数,即不被用作多态用途,通常意味着它并不意图被用作一个base class(除了某些特殊情况,如noncopyable类)。当class不被用作base class时,最好不要为其定义一个析构函数。
因为C++中将函数定义为virtual是有代价的,这个代价就是虚表指针virtual table pointer。
 
欲实现virtual函数,对象必须携带某种信息,用于在运行期决定调用哪一个virtual函数。这份信息通常是由一个所谓的vptr(virtual table pointer)指针指出。vptr指向一个有函数指针构成的数组,成为vtbl(virtual table);每一个带有virtual函数的class都有一个相应的vtbl。当对对象调用某一virtual函数,是及被调用的函数取决于该对象的vptr所指向的那个vtbl——编译器在其中寻找适当的函数指针。
 
因此,每一个定义了virtual函数的class的对象都包含一个vptr。这样一来,对象的体积会因为virtual函数的存在而增加。
 
例如:
class point
{
public:
point(int coord_x, int coord_y);
~point();
private:
int x, y;
};
32位系统中,int类型占32bits,因此point对象一共占64bits,可以被塞入一个64bit缓存器中,甚至可以被当作一个“64bit 量”传给其他语言如C活着FORTRAN编写的函数。
但是如果point内含析构函数时,point对象占用的空间将是96bits,(2个ints加1个vptr)。对象体积从64bits增加到96bits。
而在64bit计算机体系结构中,point对象将占用128bits(因为指针类型占用64bits)。对象体积从64bits增加到128bits。
 
这样的对象将无法被塞入一个64-bit缓存器中,而C++的point对象也不再和其他语言(如C)内的相同声明有着一样的结构,因此也就无法将其传递到其他语言编写的函数中,因此不再有移植性。
因此,将不用作多态用途的class的析构函数声明为virtual是不合理的。只有当class内至少含有一个virtual函数时才应该将其析构函数声明为virtual。
 
  • [引申2]
不要试图继承任何带有non-virtual析构函数的类,包括所有STL容器如vector,list, set, unordered_map, string等等。因为这会导致资源泄漏!
不幸的是C++中没有提供类似java的final classes或者c#中的sealed classes那样的“禁止派生”机制。
 
  • [引申3]
当希望将一个class定义为抽象class(pure virtual class),但有没有任何pure virtual函数时,为这个class声明一个pure virtual析构函数是很便利的。
class abstract_class
{
public:
virtual ~abstract_class() = ;
};
但是要注意:必须为这个pure virtual析构函数提供一份定义:
abstract_class::~abstract_class(){}
 
因为析构函数的运作方式是:最深层派生(most derived)的那个class的析构函数最先被调用,然后是其每一个base class的析构函数被调用。编译器会在 abstract_class的derived classes中创建一个对~abstract_class的调用动作,所以必须为~abstract_class提供定义,否则链接器会报错。
 
  • [总结]
1.polymorphic (带多态性质的)base classes 应该声明一个virtual析构函数。如果class 带有任何virtual函数,就应该为其声明一个virtual析构函数。因为这样的base class设计出来的目的就是用来“通过base class 接口处理derived class对象”。
2.有些class原本就不是设计作为base class使用,或者就算是作为base class 也不具备多态性,这样的class就不应该声明为virtual析构函数。
 
  • [补充]
默认生成的析构函数是public且non-virtual的。
 

[Effective C++系列]-为多态基类声明Virtual析构函数的更多相关文章

  1. [Effective C++ --007]为多态基类声明virtual析构函数

    引言: 我们都知道类的一个很明显的特性是多态,比如我们声明一个水果的基类: class Fruit { public: Fruit() {}; ~Fruit(){}; } 那么我们根据这个Fruit基 ...

  2. Effective C++(7) 为多态基类声明virtual析构函数 or Not

    问题聚焦: 已经对一个对象执行了delete语句,还会发生内存泄漏吗? 先来看个demo: // 计时器类 class TimeKeeper { public: TimeKeeper(); ~Time ...

  3. 为多态基类声明virtual析构函数

    一个函数的返回值为基类指针,而当指针指向一个派生类对象,接下来派生类对象被这个基类指针删除的时候,就出现了局部销毁的问题.因为C++指出,当派生类经由一个基类指针被删除,而该基类指针带着一个non-v ...

  4. effective c++(07)之为多态基类声明virtual析构函数

    class TimeKeeper { public: TimeKeeper() ; ~TimeKepper() ; ... } ; class AtomicClock:public TimeKeepe ...

  5. Effective C++_笔记_条款07_为多态基类声明virtual析构函数

    (整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 这个规则只适用于polymorphic(带多态性质的)base ...

  6. Effective C++ -----条款07:为多态基类声明virtual析构函数

    polymorphic(带多态性质的)base classes应该声明一个virtual析构函数.如果class带有任何virtual函数,它就应该拥有一个virtual析构函数. Classes的设 ...

  7. 【C++】为多态基类声明virtual析构函数

    来自<Effective C++>条款07:为多态声明virtual析构函数 当derived class对象经由一个base class指针被删除,而该base class带着一个non ...

  8. 条款7:为多态基类声明virtual析构函数

    C++明确指出:当派生类对象是由一个基类指针释放的,而基类中的析构函数不是虚函数,那么结果是未定义的.其实我们执行时其结果就是:只调用最上层基类的析构函数,派生类及其中间基类的析构函数得不到调用. # ...

  9. NO.6: 为多态基类声明virtual析构函数

    注意:polymorphic base class 应该具有虚析构函数,如果class带有任何virtual函数,也应具有虚析构函数 class不具备polymorphic属性则不应该声明virtua ...

随机推荐

  1. 转帖:深入理解JavaScript系列

    感觉汤姆大叔这个系列写的很是不错,很适合有js基础但是想深入又无从下手的朋友. 深入理解JavaScript系列

  2. Lucene学习总结之三:Lucene的索引文件格式(1)

    Lucene的索引里面存了些什么,如何存放的,也即Lucene的索引文件格式,是读懂Lucene源代码的一把钥匙. 当我们真正进入到Lucene源代码之中的时候,我们会发现: Lucene的索引过程, ...

  3. 学习PrintWriter类 .

    java.io包 1)首先先知道它的八种构造方法,但怎么记住这八种呢?我们都知道PrintWriter是一种过滤流,也叫处理流.也就是能对字节流和字符流进行处理,所以它会有: PrintWriter( ...

  4. Linux系统的命令别名功能

    命令别名功能在管理和维护Linux系统的过程中,将会使用到大量命令,有一些很长的命令或用法经常被用到,重复而频繁地输入某个很长命令或用法是不可取的.这时可以使用命令别名功能将这个过程简单化. 1.系统 ...

  5. im命令合集

    命令历史 以:和/开头的命令都有历史纪录,可以首先键入:或/然后按上下箭头来选择某个历史命令. 启动vim 在命令行窗口中输入以下命令即可 vim 直接启动vim vim filename 打开vim ...

  6. 跪求 EF缓存问题 也不知道是不是缓存 问题

    1.数据库里面查的结果 2 vs里面查的结果   为什么 不是16 而是2 下面是代码 /// <summary> /// 权限检查 /// </summary> public ...

  7. Nginx的Upload上传模块

    前段时间做一个项目,需要上传文件,差不多需要20M左右,普通用php处理会比较麻烦,经常超时,而且大量占用资源.于是搜索了下,决定用nginx的upload上传模块来处理. 你可以在这里:http:/ ...

  8. linux下源码安装apache服务

    1.搭建静态网站是,我们只需要搭建apache服务即可满足要求. 例如:如果我再客户端游览器输入地址,他会找到192.168.1.100这个服务器,然后根据端口会找到apache服务器.apache他 ...

  9. 互斥体与互锁 <第五篇>

    互斥体实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mutex)).互斥体禁止多个线程同时进入受保护的代码“临界区”.因此,在任意时刻,只有一个线程被允许进入这 ...

  10. RegexOptions枚举

    在创建Regex类的实例时,构造函数的重载中有一个要求传入RegexOptions的一个枚举值,我相信这个枚举一定非常有用,否则不会要求在构造函数中传入.今天就来看一看这个枚举的作用. 我们干脆把代码 ...