Effective C++条款07:为多态基类声明virtual析构函数

本文详细解释了为何在基类中使用virtual析构函数的重要性,包括避免内存泄漏、确保派生类正确销毁等方面,并探讨了在不同场景下使用virtual析构函数的原则。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

  • 我们创建一个TimeKeeper基类一些及其它的派生类作为不同的计时方法
 
  1. class TimeKeeper

  2. {

  3. public:

  4. TimeKeeper() {}

  5. ~TimeKeeper() {} //非virtual的

  6. };

  7.  
  8. //都继承与TimeKeeper

  9. class AtomicClock :public TimeKeeper{};

  10. class WaterClock :public TimeKeeper {};

  11. class WristWatch :public TimeKeeper {};

  • 如果客户想要在程序中使用时间,不想操作时间如何计算等细节,这时候我们可以设计factory(工厂)函数,让函数返回指针指向一个计时对象。该函数返回一个基类指针,这个基类指针是指向于派生类对象的
 
  1. TimeKeeper* getTimeKeeper()

  2. {

  3. //返回一个指针,指向一个TimeKeeper派生类的动态分配对象

  4. }

  • 因为函数返回的对象存在于堆中,因此为了在不使用时我们需要使用释放该对象(delete)
 
  1. TimeKeeper* ptk = getTimeKeeper();

  2.  
  3. delete ptk;

  • 此处基类的析构函数是非virtual的,因此通过一个基类指针删除派生类对象是错误的
  • 解决办法:将基类的析构函数改为virtual就正确了
 
  1. class TimeKeeper

  2. {

  3. public:

  4. TimeKeeper() {}

  5. virtual ~TimeKeeper() {}

  6. };

  • 声明为virtual之后,通过基类指针删除派生类对象就会释放整个对象(基类+派生类) 

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

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

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

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

  • 我们在多态文章中说过(参阅:https://blog.youkuaiyun.com/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容器产生的错误

演示案例

 
  1. //继承于string,string没有虚析构函数

  2. class SpecialString :public std::string{};

  3.  
  4.  
  5. SpecialString* pss = new SpecialString;

  6. std::string *ps = pss;

  7.  
  8. delete ps; //错误

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

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

演示案例

 
  1. //抽象类

  2. class AWOV

  3. {

  4. public:

  5. virtual ~AWOV() = 0;

  6. };

  7.  
  8. //需要给出一份定义

  9. AWOV::~AWOV(){}

  10.  
  11. class A :public AWOV {};

  12.  
  13.  
  14. AWOV* p = new A;

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

六、总结

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值