读书笔记 effective c++ Item 7 在多态基类中将析构函数声明为虚析构函数

小编 2026-06-22 阅读:584 评论:0
1. 继承体系中关于对象释放遇到的问题描述1.1 手动释放关于时间记录有很多种方法,因此为不同的...

1. 继承体系中关于对象释放遇到的问题描述

1.1 手动释放

关于时间记录有很多种方法,因此为不同的计时方法创建一个TimeKeeper基类和一些派生类就再合理不过了:

 1 class TimeKeeper { 2  3 public: 4  5 TimeKeeper(); 6  7 ~TimeKeeper(); 8  9 ...10 11 };12 13 class AtomicClock: public TimeKeeper { ... };14 15 class WaterClock: public TimeKeeper { ... };16 17 class WristWatch: public TimeKeeper { ... };18 19  

许多客户端只想访问时间而不想知道关于时间计算的细节,所以可以创建一个工厂方法,这个工厂方法返回一个指向新创建的派生类对象的基类指针,这个指针用来指向一个计时对象:

1 TimeKeeper* getTimeKeeper(); // returns a pointer to a dynamic-2 3                                                          // ally allocated object of a class4 5                                                           // derived from TimeKeeper

为了和工厂方法的约定保持一致,getTimeKeeper返回一个堆上的对象,因此为了避免泄露内存和其他资源,每个返回的对象被合理的释放掉(deleted)是很重要的:

1 TimeKeeper *ptk = getTimeKeeper(); // get dynamically allocated object2 3 // from TimeKeeper hierarchy4 5 ... // use it6 7 delete ptk; // release it to avoid resource leak

Item13中解释到依赖客户执行deletion比较容易出错,在Item18中解释了如何改变工厂函数的接口来预防一般的客户端错误,但是这些关注点在这里都是次要的,因为在这个条款中,我们为上面的代码提出一个更基本的弱点:即使客户端把一切都做对了,根本没有方法去知道程序如何运转。

1.2非虚析构函数引入的问题

问题在于getTimeKeeper返回一个指向派生类对象的指针(AtomicClock),这个对象通过一个基类指针(一个TimeKeeper*指针)来进行释放(delete),基类中(TimeKeeper)有一个非虚析构函数。这是造成灾难的一个因素,因为c++指出:通过一个基类的指针来释放一个派生类的对象,如果基类的析构函数是非虚的,那么结果未定义。在运行时有可能发生以下状况:对象的派生类部分永远不会被释放掉。如果对getTimeKeeper的调用恰巧返回一个指向AtomicClock对象的指针,对象的AtomicClock部分(也就是在AtomicClock类中声明的数据成员)可能不会被释放掉,AtomicClock类的析构函数也不会被执行。然而,基类部分(也就是TimeKeeper部分)是会被释放掉的,这会导致产生一个古怪的“部分被释放的”对象。这是使资源泄露,破坏数据结构和在debugger上花费大把时间的绝佳方法。

2.如何解决问题-声明虚析构函数

消除这个问题很简单:为基类提供一个虚析构函数。这时如果delete一个派生类对象将会做到你想要的。它会释放掉整个对象,包括派生类的所有部分:

 1 class TimeKeeper { 2  3 public: 4  5 TimeKeeper(); 6  7 virtual ~TimeKeeper(); 8  9 ...10 11 };12 13 TimeKeeper *ptk = getTimeKeeper();14 15 ...16 17 delete ptk; // now behaves correctly

基类中(TimeKeeper)除了析构函数外一般情况下会包含虚函数,因为虚函数存在的目的是为了函数在派生类中的定制化实现(Item34。举个例子,TimeKeeper会有一个虚函数,getCurrentTime,这个函数在不同的派生类中会有不同的实现。任何有虚函数的类应该肯定有一个虚析构函数。

 

3.不要在不当作基类的类中声明虚析构函数

 

如果类中不包含虚函数,这通常表明它不会被用作基类,如果并没有打算将一个类作为一个基类,将析构函数声明为虚是一个坏的想法。考虑一个表示二维空间的点的类:

 1 class Point { // a 2D point 2  3 public: 4  5 Point(int xCoord, int yCoord); 6  7 ~Point(); 8  9 private:10 11 int x, y;12 13 };

如果int占用32Bits,那么一个Point对象可被放入一个64-bit的缓存器中。并且,这个Point对象可以以一个”64-bit quantity”传给用其他语言编写的函数,例如c语言和Fortran。如果将Point的析构函数声明成虚的,状况就会发生变化。

虚函数的实现需要对象带一些信息,根据这些信息在运行时能够决定对象的哪个虚函数会被触发。这些信息表现为一个被叫做vptr(virtual table pointer)指针的形式。我们把指向一个函数指针数组的vptr指针叫做vtbl(virtual table);每个有虚函数的类都有一个关联的vtbl.当虚函数在一个对象上被触发,实际调用的函数是由对象的vtbl中的vptr来决定的,在vtbl中会查找到合适的函数指针。

关于虚函数是如何实现的细节并不重要。重要的是如果Point类中包含一个虚函数,这个类型的对象会在占用空间上有所增加:在32位机器中,空间会从64bits(两个int)增加到96bits;在64位机器中,空间会从64bits增加到128bits,因为64位机器上的指针在空间上占用64bits.Point额外增加了一个vptr而致使内存空间增加50-100%。Point将不能在放进64bits的缓存中。并且,c++中的Point也不再同其他语言(如C语言)中声明的对象有类似的结构了,因为其他语言没有vptr,因此你不再能够向(从)其他语言编写的函数中传进(传出)指针了,除非你对vptr进行明确的补偿,这属于实现细节,代码因此也不能够被移植了。

因此,无缘无故的将所有析构函数声明成虚函数同永远不将其声明为虚函数犯了一样的错误。事实上,许多人将上面的情形其总结如下:在类中声明虚析构函数当且仅当类中至少包含一个虚函数。

4.不要继承析构函数为非虚的类

在虚函数完全缺席的情况下,非虚析构函数的问题同样会导致只释放部分内存的问题。举个例子,标准string类型不包含虚函数,但是一些被误导的程序员有时会将其当作基类:

1 class SpecialString: public std::string { // bad idea! std::string has a2 3 ... // non-virtual destructor4 5 };

乍一看这么实现也许无伤大雅,但是如果在一个应用中的某个地方,你以某种方式将指向SpecialString的指针转换成指向string的指针,然后你在string指针上使用delete,你马上会被转到未定义行为的领地:

 1 SpecialString *pss =new SpecialString("Impending Doom"); 2  3 std::string *ps; 4  5 ... 6  7 ps = pss; // SpecialString* ⇒ std::string* 8  9 ...10 11 delete ps; // undefined! In practice,12 13 // *ps’s SpecialString resources14 15 // will be leaked, because the16 17 // SpecialString destructor won’t18 19 // be called

同样的分析适用于任何缺少虚析构函数的类,包含所有的STL容器类型(例如 vector,list set,tr1::unordered_map(Item54))。如果你曾经受到诱惑,从一个标准容器类或其他没有虚析构函数的类中继承,你需要抵抗这种诱惑!(不幸的是,c++没有提供不能继承的机制,java中有final类,c#中有sealed类)。

5.纯虚析构函数

偶尔情况下为类提供一个纯虚析构函数是很方便的。有纯虚函数的类是一个抽象类,其不能够被实例化。然而有时候,你想将一个类变成一个抽象类,但是没有任何纯虚函数。该怎么办?因为一个抽象类将来会被用作基类,并且基类应该有一个虚析构函数,同时一个纯虚函数产生一个抽象类,所以解决方案很简单:在你想要其变成抽象的类中声明一个纯虚析构函数。看下面的例子:

1 class AWOV { // AWOV = “Abstract w/o Virtuals”2 3 public:4 5 virtual ~AWOV() = 0; // declare pure virtual destructor6 7 };

这个类有一个纯虚函数,所以它是抽象类。因为它有一个虚析构函数,所以你不必担心因为析构函数出现的问题。这里有个窍门,你必须为纯虚函数提供一份定义

1 AWOV::~AWOV() {} // definition of pure virtual dtor

析构函数工作的方法是最底部的派生类先被调用,然后析构函数的每一个基类会被依次调用。编译器会从派生类的析构函数中生成一个对~AWOV的调用,因此你必须确保为这个函数提供一个函数体。如果不提供会有链接错误。

6.其他一些需要注意的地方

 为基类提供虚析构函数的法则只适用于多态基类,多态基类也就是将基类设计成允许通过基类接口来操作派生类型的类。TImeKeeper是一个多态基类,因为我们想能够操作AtomicClokc和WaterClock对象,在即使只有TimeKeeper指针指向这些派生类对象的情况下。

并不是所有的基类都被设计成能够使用多态。举个例子,标准string类型还有STL容器类型并没有被设计成基类,更不用说多态了。一些类被设计成当基类使用,但是没有被设计成使用多态。举个例子,Item6中的UnCopyable和来自标准库中的input_iterator_tag(Item47),这样的类没有被设计成通过基类接口操作派生类。因此,也不需要虚析构函数。


作者:HarlanC

博客地址:http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

版权声明

本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。

热门文章
  • 机房智能化温湿度解决方式之POE供电以太网温湿度传感器

    机房智能化温湿度解决方式之POE供电以太网温湿度传感器
    机房智能化温湿度解决方式之POE供电以太网温湿度传感器 北京盈创力和电子科技有限公司 智能型TCP网口温湿度记录仪 北京IP网络温湿度记录仪厂家,北京盈创力和 北京智能型TCP网口温湿度记录仪IP网络温湿度记录仪是一种新型的基于TCP/IP协议双绞线以太网标准温湿度采集模块,利用它可以实现现场温度值、相对湿度值的采集,同时利用其自身的RJ45通信接口可以方便地和机房监控主机或交换机集线器进行联网。 工作于-40℃~85℃工业级带...
  • Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering

    Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering
    Problem Statement 我们考虑一个具有马尔可夫性质、非线性、非高斯的状态空间模型(State Space Model):对于一个时间序列上的观测结果{yt,t∈N}\\{ y_t , t \\in N \\}{yt​,t∈N},我们认为每个观测结果yty_tyt​的生成依赖于一个无法直接观察的隐变量xt∈{xt,t∈N}x_t \\in \\{x_t , t \\in N \\}xt​∈{xt​,t∈N},即:p(...
  • HTTP状态保持的原理

    HTTP状态保持的原理
    a)在用户登录之后,浏览器返回响应的时候会在响应中添加上cookieb)浏览器接收到cookie之后会自动保存c)当用户再次请求同一服务器中的其他网页的时候,浏览器会自动带上之前保存的cookied)服务接收到请求之后可以请 request 对象中取到cookie 判断当前用户是否登录  Http是无状态的,就是连接时数据互通,关闭后...
  • Hive 系统函数及示例

    Hive 系统函数及示例
    查看所有系统函数 show functions; 函数分类 内置函数【系统函数】 数学函数: floor、round、ceil、cos、log2等 字符串函数: length、reverse、trim、lower、get_json_object、repeat等 收集函数: size 转换函数: cast 日期函数: year、month、datediff、date、date_add等 条件函数: coalesce、case…w...
  • CSRF的原理和防范措施

    CSRF的原理和防范措施
    a)攻击原理:i.用户C访问正常网站A时进行登录,浏览器保存A的cookieii.用户C再访问攻击网站B,网站B上有某个隐藏的链接或者图片标签会自动请求网站A的URL地址,例如表单提交,传指定的参数iii.而攻击网站B在访问网站A的时候,浏览器会自动带上网站A的cookieiv.所以网站A在接收到请求之后可判断当前用户是登录状态,所以...
标签列表