读书笔记 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

相关文章

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

C/C++错误信息处理的常见方法及函数

《C/C++错误信息处理的常见方法及函数》C/C++是两种广泛使用的编程语言,特别是在系统编程、嵌入式开发以及高性能计算领域,:本文主要介绍C/C++错误信息处理的常见方法及函数,文中通过代码介绍... 目录前言1. errno 和 perror()示例:2. strerror()示例:3. perror(

C++变换迭代器使用方法小结

《C++变换迭代器使用方法小结》本文主要介绍了C++变换迭代器使用方法小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1、源码2、代码解析代码解析:transform_iterator1. transform_iterat

详解C++中类的大小决定因数

《详解C++中类的大小决定因数》类的大小受多个因素影响,主要包括成员变量、对齐方式、继承关系、虚函数表等,下面就来介绍一下,具有一定的参考价值,感兴趣的可以了解一下... 目录1. 非静态数据成员示例:2. 数据对齐(Padding)示例:3. 虚函数(vtable 指针)示例:4. 继承普通继承虚继承5.

C++中std::distance使用方法示例

《C++中std::distance使用方法示例》std::distance是C++标准库中的一个函数,用于计算两个迭代器之间的距离,本文主要介绍了C++中std::distance使用方法示例,具... 目录语法使用方式解释示例输出:其他说明:总结std::distance&n编程bsp;是 C++ 标准

Kotlin 作用域函数apply、let、run、with、also使用指南

《Kotlin作用域函数apply、let、run、with、also使用指南》在Kotlin开发中,作用域函数(ScopeFunctions)是一组能让代码更简洁、更函数式的高阶函数,本文将... 目录一、引言:为什么需要作用域函数?二、作用域函China编程数详解1. apply:对象配置的 “流式构建器”最

C++ 中的 if-constexpr语法和作用

《C++中的if-constexpr语法和作用》if-constexpr语法是C++17引入的新语法特性,也被称为常量if表达式或静态if(staticif),:本文主要介绍C++中的if-c... 目录1 if-constexpr 语法1.1 基本语法1.2 扩展说明1.2.1 条件表达式1.2.2 fa

Android Kotlin 高阶函数详解及其在协程中的应用小结

《AndroidKotlin高阶函数详解及其在协程中的应用小结》高阶函数是Kotlin中的一个重要特性,它能够将函数作为一等公民(First-ClassCitizen),使得代码更加简洁、灵活和可... 目录1. 引言2. 什么是高阶函数?3. 高阶函数的基础用法3.1 传递函数作为参数3.2 Lambda

C++中::SHCreateDirectoryEx函数使用方法

《C++中::SHCreateDirectoryEx函数使用方法》::SHCreateDirectoryEx用于创建多级目录,类似于mkdir-p命令,本文主要介绍了C++中::SHCreateDir... 目录1. 函数原型与依赖项2. 基本使用示例示例 1:创建单层目录示例 2:创建多级目录3. 关键注

C++从序列容器中删除元素的四种方法

《C++从序列容器中删除元素的四种方法》删除元素的方法在序列容器和关联容器之间是非常不同的,在序列容器中,vector和string是最常用的,但这里也会介绍deque和list以供全面了解,尽管在一... 目录一、简介二、移除给定位置的元素三、移除与某个值相等的元素3.1、序列容器vector、deque