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

本文主要是介绍读书笔记 effective c++ Item 7 在多态基类中将析构函数声明为虚析构函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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 class
4 
5                                                           // derived from TimeKeeper

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

1 TimeKeeper *ptk = getTimeKeeper(); // get dynamically allocated object
2 
3 // from TimeKeeper hierarchy
4 
5 ... // use it
6 
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 point2 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 a
2 
3 ... // non-virtual destructor
4 
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 resources
14 
15 // will be leaked, because the
16 
17 // SpecialString destructor won’t
18 
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 destructor
6 
7 };

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

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

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

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

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

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

这篇关于读书笔记 effective c++ Item 7 在多态基类中将析构函数声明为虚析构函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/882657

相关文章

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

GO语言短变量声明的实现示例

《GO语言短变量声明的实现示例》在Go语言中,短变量声明是一种简洁的变量声明方式,使用:=运算符,可以自动推断变量类型,下面就来具体介绍一下如何使用,感兴趣的可以了解一下... 目录基本语法功能特点与var的区别适用场景注意事项基本语法variableName := value功能特点1、自动类型推

GO语言中函数命名返回值的使用

《GO语言中函数命名返回值的使用》在Go语言中,函数可以为其返回值指定名称,这被称为命名返回值或命名返回参数,这种特性可以使代码更清晰,特别是在返回多个值时,感兴趣的可以了解一下... 目录基本语法函数命名返回特点代码示例命名特点基本语法func functionName(parameters) (nam

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Python Counter 函数使用案例

《PythonCounter函数使用案例》Counter是collections模块中的一个类,专门用于对可迭代对象中的元素进行计数,接下来通过本文给大家介绍PythonCounter函数使用案例... 目录一、Counter函数概述二、基本使用案例(一)列表元素计数(二)字符串字符计数(三)元组计数三、C

python中的显式声明类型参数使用方式

《python中的显式声明类型参数使用方式》文章探讨了Python3.10+版本中类型注解的使用,指出FastAPI官方示例强调显式声明参数类型,通过|操作符替代Union/Optional,可提升代... 目录背景python函数显式声明的类型汇总基本类型集合类型Optional and Union(py

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

Python中的filter() 函数的工作原理及应用技巧

《Python中的filter()函数的工作原理及应用技巧》Python的filter()函数用于筛选序列元素,返回迭代器,适合函数式编程,相比列表推导式,内存更优,尤其适用于大数据集,结合lamb... 目录前言一、基本概念基本语法二、使用方式1. 使用 lambda 函数2. 使用普通函数3. 使用 N

MySQL中REPLACE函数与语句举例详解

《MySQL中REPLACE函数与语句举例详解》在MySQL中REPLACE函数是一个用于处理字符串的强大工具,它的主要功能是替换字符串中的某些子字符串,:本文主要介绍MySQL中REPLACE函... 目录一、REPLACE()函数语法:参数说明:功能说明:示例:二、REPLACE INTO语句语法:参数

C++ vector越界问题的完整解决方案

《C++vector越界问题的完整解决方案》在C++开发中,std::vector作为最常用的动态数组容器,其便捷性与性能优势使其成为处理可变长度数据的首选,然而,数组越界访问始终是威胁程序稳定性的... 目录引言一、vector越界的底层原理与危害1.1 越界访问的本质原因1.2 越界访问的实际危害二、基