[Effective C++系列]-为多态基类声明Virtual析构函数

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掉:java

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

首先须要说明,上述作法已经存在两个缺陷:c#

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() = 0;
};
可是要注意:必须为这个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的。
相关文章
相关标签/搜索