一、从一个例子中介绍为什么要为基类使用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
如果将基类的析构函数声明为virtual
我们在多态文章中说过(参阅:C++:85---类继承(多态)_董哥的黑板报的博客-CSDN博客 ),如果将基类指针/引用绑定于派生类,此时通过这个指针/引用调用虚函数,那么这个虚函数的调用与指针/引用所指向的类型有关,因此当通过基类的指针释放(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析构函数