【effective C++】07、为多态基类声明virtual析构函数

1、多态时virtual析构函数的必要性

1.1、问题描述

下面的例子,是关于类的继承和动态绑定:

class TimeKeeper{
    public:
        TimeKeeper();
        ~TimeKeeper();
        //~~~
};
class AtomicClock: public TimeKeeper{/*~~~*/};   //原子钟
class WaterClock: public TimeKeeper{/*~~~*/};    //水钟
class WristClock: public TimeKeeper{/*~~~*/};    //腕表

通过设计factory(工厂)函数,返回一个指针指向一个计时对象,factory函数会返回一个base class的指针,指向新生成的derived class对象:

TimeKeeper* getTimeKeeper();//返回一个指针指向一个TimeKeeper派生类的动态分配对象

TimeKeeper* ptk = getTimeKeeper();
//~~~
delete ptk;

如上面的例子,getTimeKeeper返回的指针指向一个derived class对象,而那个对像却经由一个base calss的指针删除,而base class有一个non-virtual的析构函数,这样会带来一个严重的问题:当derived class 对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,实际执行时通常发生的是对象的derived成分没有被销毁。如果getTimeKeeper返回的指针指向一个AtomicClock对象,那么AtomicClock的成分(该类内部的成员变量)很可能没有被销毁,这就造成的局部销毁,造成资源泄漏。

1.2、解决方法

class TimeKeeper{
    public:
        TimeKeeper();
        virtual ~TimeKeeper();
        //~~~
};

TimeKeeper* getTimeKeeper();//返回一个指针指向一个TimeKeeper派生类的动态分配对象

TimeKeeper* ptk = getTimeKeeper();
//~~~
delete ptk; //现在行为正确

任何class只要带有virtual函数都几乎确定应该有一个virtual析构函数。

2、不要盲目的令类的析构函数为virtual

class Point{
    public:
        Point(int xCode, int yCoord);
        ~Point();
    private:
        int x, y;   
};

如果int占用32bits,那么point可以放入一个64-bits的缓存器中,这个Point对象甚至可被当作一个64-bits的量传递给其他语言如C撰写的函数。
但是当Point的析构函数为virtual,形势就会发生变化:
实现出virtual函数,对象就会携带某些信息,主要是在运行期间决定哪一个virtual函数应该被调用。这个信息通常由vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table);每一个带有virtual函数的class都有一个相应的vtbl,当对象调用某一个virtual函数,实际被调用的函数取决于该对象的vptr所指向的那个vtbl —- 编译器在其中寻找适当的函数指针。
上面的例子,如果是32-bits计算机体系结构,将原来的占有64bits的空间变为96bits(64+32[vptr]);而在64-bits计算机体系结构,将原来的占有64bits的空间变为128bits(64+64[vptr])。这时,Point对象无法放入一个64-bits的缓存器中,而C++的Point对象也不再和其他语言(如C)内的相同声明有着一样的结构,就无法将其传递给其他语言实现的函数,也就不再具有移植性。

无端的将所有的class的析构函数设置为virtual也是错误的,只有当class内部至少含有一个virtual函数的时候才为其声明为virtual析构函数。

3、类的不可继承性

class SpecialString: public std::string{
    //~~~
};

SpecialString* pss = new SpecialString("Impending Doom");
std::string* ps;
//~~~
ps = pss;
delete ps;
//现实中*ps的SpecialString资源会泄漏,因为SpecialString析构函数没有被调用。

基于任何不带virtual的析构函数的class,包括所有的STL容器如vector、list、set、tr1::unorderd_map等等,如果你企图继承一个标准的容器或者任何其他non-virtual析构函数的class,都有可能出现上述的问题。

4、纯虚函数(pure virtual)

纯虚函数导致抽象类(abstract classes),这种类不能被实例话,不能创建这种类的对象,总是企图被当作一个base class来用,但同时base class应该有一个virtual的析构函数,所以最简单的解决方式就是:为希望成为抽象的类声明一个pure virtual的析构函数。

class AWOV{
    public:
        virtual ~AWOV() = 0;//声明为pure virtual的析构函数         
};

这个类有一个pure virtual函数,所以他是一个抽象类,同时又由于它有virtual析构函数,所以无需担心析构函数的问题,但这里有一点就是要为这个pure virtual析构函数提供一份定义:

AWOV::~AWOV(){}        //pure virtual析构函数的定义

析构函数的运作方式:最深层派生的class的析构函数最先被调用,然后是其每一个base class的析构函数被调用。编译器会在AWOV的派生类的析构函数中创建一个对~AWOV的调用动作,所以必须为这个函数提供一份定义。

⚠️注意:
并非所有的base class的设计目的是为了多态的用途,如标准的string和STL容器甚至不会作为base class使用。很多的类设计的目的是作为base class但不是用于多态,因此不需要virtual的析构函数。

5、总结

(1)带有多态性的base class应该声明一个virtual析构函数;如果class带有任何的virtual函数,那么这个类应该拥有一个virtual的析构函数。
(2)class的设计目的并不是作为base class使用或者不是为了具备多态性,那就不应该声明为virtual析构函数。

猜你喜欢

转载自blog.csdn.net/u013108511/article/details/80573470
今日推荐