Item7: Declare destructors virtual in polymorphic base classes

来源:互联网 发布:1password mac 编辑:程序博客网 时间:2024/06/08 06:12

看一个计时类:

 

许多客户只想在程序中使用时间,不想操心时间如何计算等细节,这时候我们可以设计factory function(工厂函数)用来返回指针指向一个TimeKeeper对象。factory function会“返回一个base class指针,指向新生成的derived class对象”:

 

 为遵守factory function(工厂函数)的规矩,getTimeKeeper()返回的对象是建立在heap上的。所以为了避免泄露内存和其他资源,将factory function(工厂函数)返回的每一个对象适当的delete掉很重要:

 

条款13说指望用户执行delete动作很容易出错(error-prone),条款18也说到了factory function(工厂函数)接口该如何修改以便预防常见的客户错误,不过在这里,这些都是次要的。我们将要应付的是上述代码的一个更根本弱点:纵使用户把每件事都做对了,仍然没有办法知道程序如何行动。

 

     问题就出在于getTimeKeeper返回的指针指向一个derived class对象(例如AtomicClock),而那个对象却经由一个base class指针(例如一个TimeKeeper*指针)被删除。而且目前的base class(TimeKeeper)有个non-virtual析构函数。

 

 

    这会引来灾难。因为C++明确指出,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未定义(results are undefined)——实际执行时通常发生的是对象的derived成分没被销毁。如果getTimeKeeper返回指针指向一个AtomicClock对象,其内的AtomicClock成分(也就是声明于AtomicClock class内的成员变量)很可能没被销毁,而AtomicClock的析构函数也未被执行。然而其base class成分(也就是TimeKeeper这一部分)通常会被销毁,于是造成了一个诡异的“局部销毁”对象(a curious "partially destroyed" object)。这可是形成资源泄露、败坏数据结构、在调试器上浪费许多时间的绝佳途径喔。

 

     怎么样消除这个问题呢?

     很简单:给base class一个virtual析构函数。此后删除derived class对象就会如你想要的那般。是的,它会销毁整个对象,包括所有derived class成分:

 

像TimeKeeper这样的base classes除了析构函数之外通常还有其他的virtual函数,因为virtual函数的目的是允许derived class的实现得以定制化(because the purpose of virtual functions is to allow customization of derived class implementations)。例如TimeKeeper就可能拥有一个virtual getCurrentTime,它在不同的derived classes中有不同的实现代码。任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。

 

      如果class不含virtual函数,通常表示它并不意图被用作一个base class。当class不企图被当作base class,令其析构函数为virtual往往是一个馊主意。考虑一个用来表示二维空间点坐标的class:

 

如果int占用32bits,那么Point对象可塞入一个64-bit缓存器中。更有甚者,这样一个Point对象可被当做一个“64-bit量”传给以其他语言如C或FORTRAN撰写的函数。然而当Point的析构函数是virtual,形势起了变化。

 

      欲实现virtual函数,对象必须携带某些信息,主要用来在运行期决定哪个virtual函数该被调用。这份信息通常是由一个所谓vptr(virtual table pointer)指针给出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table);每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某个virtual函数,实际上被调用的函数取决于该对象的vptr所指的那个vtbl——编译器在其中寻找适当的函数指针。

 

      如果Point class内含virtual函数,其对象的体积会增加:在32-bit计算机体系结构中将占用64bits(为了存放两个ints)至96bits(两个ints加上vptr);在64-bit计算机体系结构中可能占用64~128bits,因为指针在这样的计算机结构中占64bits。因此,为Point添加一个vptr会增加其对象大小达50%~100%!Point对象不再能够塞入一个64-bit缓存器,而C++的Point对象也不同于和其他语言(如C)内的相同声明有着一样的结构(因为其他语言的对应物并没有vptr),因为也就不再可能把它传递至(或接受自)其他语言所写的函数,除非你明确补偿vptr——那属于实现细节,也因此不再具有移植性。

 

      因此,无端地将所有classes的析构函数声明为virtual,就像从未声明它们为virtual一样,都是错误的。许多人的心得是:只有当class内含至少一个virtual函数,才为它声明virtual析构函数。

 

 

      It is possible to get bitten by the non-virtual destructor problem even in the complete absence of virtual functions. 例如,标准string不含任何virtual函数,但有时候程序员会错误地把它当作base class:

 

然后你这样操作:

 

      相同的分析适用于任何不带virtual析构函数的class,包括所有STL容器如vector,list,set,trl::unordered_map等等。如果你曾经企图继承一个标准容器或任何其他“带有non-virtual析构函数”的class,拒绝诱惑吧!

 

 

      有时候令class带一个pure virtual析构函数,可能颇为便利。pure virtual函数导致abstract classes(抽象类)——也就是不能被实体化(instantiated)的class。也就是说,你不能为那种类型创建对象。然而有时候你希望拥有abstract class,但手上没有任何pure virtual函数,怎么办?唔,由于abstract class总是企图被当作一个base class来用,而又由于base class应该有个virtual析构函数,并且由于pure virtual函数会导致abstract class,因此解法很简单:为你希望它成为抽象的那个class声明一个pure virtual析构函数。下面是个例子:

但你必须为这个pure virtual析构函数提供一份定义:

 

析构函数的动作方式是,最深层派生(most derived)的那个class其析构函数最先被调用,然后是其每一个base class的析构函数被调用。编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用动作,所以你必须为这个函数提供一份定义。如果不这样做,连接器会报错。

 

 

     给base classes一个virtual析构函数,这个规则只适用于polymorphic(多态的)base classes身上。这种base classes的设计目的是为了用来“通过base classes接口处理derived class对象”。TimeKeeper就是一个多态的base class,因为我们希望处理AtomicClock和WaterClock对象,纵使我们只有TimerKeeper指针指向它们。

 

     并非所有的base classes的设计目的都是为了多态用途。例如标准string和STL容器都不被设计作为base classes使用,更别提多态了。某些classes的设计目的是作为base classes使用,但不是为了多态用途。这样的classes如条款6的Uncopyable和标准程序库的input_iterator_tag(条款47),它们并非被设计用来“经由class接口处置derived class对象”,因此它们不需要virtual析构函数。

 

请记住:

1、多态的base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个vritual析构函数。

2、classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphic),就不该声明virtual析构函数。

    

     

 

原创粉丝点击