Effective C++条款07:构造/析构/赋值运算之(为多态基类声明virtual析构函数)

一、从一个例子中介绍为什么要为基类使用virtual析构函数

  • 我们创建一个TimeKeeper基类一些及其它的派生类作为不同的计时方法
class TimeKeeper
{
public:
    TimeKeeper() {}
    ~TimeKeeper() {}  //非virtual的
};

//都继承与TimeKeeper
class AtomicClock :public TimeKeeper{};
class WaterClock :public TimeKeeper {};
class WristWatch :public TimeKeeper {};
  • 如果客户想要在程序中使用时间,不想操作时间如何计算等细节,这时候我们可以设计factory(工厂)函数,让函数返回指针指向一个计时对象。该函数返回一个基类指针,这个基类指针是指向于派生类对象的
TimeKeeper* getTimeKeeper()
{
    //返回一个指针,指向一个TimeKeeper派生类的动态分配对象
}
  • 因为函数返回的对象存在于堆中,因此为了在不使用时我们需要使用释放该对象(delete)
TimeKeeper* ptk = getTimeKeeper();

delete ptk;
  • 此处基类的析构函数是非virtual的,因此通过一个基类指针删除派生类对象是错误的
  • 解决办法:将基类的析构函数改为virtual就正确了
class TimeKeeper
{
public:
    TimeKeeper() {}
    virtual ~TimeKeeper() {}
};
  • 声明为virtual之后,通过基类指针删除派生类对象就会释放整个对象(基类+派生类) 

二、为什么将析构函数声明为virtual就正确了?

如果不将基类的析构函数声明为virtual

  • 我们在一篇文章中说过(参阅:https://blog.csdn.net/qq_41453285/article/details/103106043),将基类指针/引用绑定于派生类对象身上,那么通过这个指针/引用操作对象,操作的内容与指针/引用的类型有关,因此此时通过基类的指针释放(delete)对象,那么调用的是基类的析构函数,此时派生的析构函数没有执行,相当于只释放了基类的内存,但是派生类的内存没有释放

如果将基类的析构函数声明为virtual

  • 我们在多态文章中说过(参阅:https://blog.csdn.net/qq_41453285/article/details/103108495),如果将基类指针/引用绑定于派生类,此时通过这个指针/引用调用虚函数,那么这个虚函数的调用与指针/引用所指向的类型有关,因此当通过基类的指针释放(delete)对象时,那么调用的是派生类的析构函数(我们知道析构函数的执行顺序是:先执行派生的析构函数-->然后再执行基类的析构函数,这样才能保证在整个继承体系中把所有的内存都是释放了),因此整个派生体系的内存都释放了,因此不会造成任何内存泄漏

三、何时使用virtual析构函数

  • 如何使用virtual析构函数也是分场景的,下面分析一些场景

①继承体系中:含有virtual函数或要使用多态应该使用virtual虚析构函数

  • ①我们通常使用继承关系,就是希望在某些情况下使用“多态”。因此使用基类指针指向于派生类会常见的,因此在具有类继承的关系下,就应该为基类设计virtual析构函数虽然不是强制的,但是在使用基类指针释放子类对象时就会出错)
  • 如果基类中有虚函数,那么就强烈建议为基类设计virtual析构函数了,因此含有虚函数就说明有很大可能会用到多态(虽然也是建议,不是强制的)

②继承体系中:没有virtual函数/不使用多态可以不使用virtual虚析构函数

  • 与①介绍类似,如果你的基类被设计的时候明确:不会使用到多态,不会使用到任何virtulal函数。那么可以不为基类设计virtual虚析构函数

③没有继承关系:不要设计virtual虚析构函数

  • 如果类中有virtual,就一定会含有一个虚函数指针,因此在没有继承的关系中,使用virutal会导致对象大小增加,浪费内存
  • 另外一个原因:如果有virtual,那么C++的对象将不会与其他语言(如C语言)有着相同的结构(因为有了虚函数指针/虚函数表),因此就不能把这个对象传递给(或接受自)其他语言所编写的函数(丧失了兼容性)

四、继承于STL标准容器产生的错误

  • STL容器如vector、list、set,trl::unordered_map等等,这些容器都不带有虚析构函数,所以如果当你定义一个类继承于这些容器,然后再使用容器基类指针释放你自己定义的类对象,那么将会产生错误
  • C++没有类似于JAVA的final classes或C#的sealed classes那样的“禁止派生”机制,所以需要注意继承于STL容器产生的错误

演示案例

//继承于string,string没有虚析构函数
class SpecialString :public std::string{};


SpecialString* pss = new SpecialString;
std::string *ps = pss;

delete ps; //错误

五、virtual析构函数在抽象类中的使用

  • 我们知道,如果一个类拥有纯虚函数(=0),那么该类是一种“抽象类”,并且自身不可以被实例化
  • 此处介绍一种virtual析构函数在类中的使用:有时候我们希望定义一个抽象类,但是抽象类必须要有纯虚函数,那么此时怎么办呢?此时我们可以将类的析构函数定义为纯虚函数
  • 如果通过抽象类指针释放派生类对象:那么抽象类的析构函数虚不仅要设为virtulal,还需要给出一份定义

演示案例

//抽象类
class AWOV
{
public:
	virtual ~AWOV() = 0;
};

//需要给出一份定义
AWOV::~AWOV(){}

class A :public AWOV {};


AWOV* p = new A;
delete p;       //正确(因为AWOV也要释放,所以需要定义AWOV的析构函数)

六、总结

  • 带多态性质的基类应该声明一个virtual析构函数或如果类带有任何virtual函数,也应该拥有一个virtual析构函数
  • 类的设计目的如果不是作为基类使用,或不是为了具备多态性,那就不该声明virtual析构函数

你可能感兴趣的:(Effective,C++)