当前位置: 首页 > news >正文

Effective C++条款07——为多态基类声明virtual析构函数(构造/析构/赋值运算)

有许多种做法可以记录时间,因此,设计一个TimeKeeper base class和一些derived classes 作为不同的计时方法,相当合情合理:

class TimeKeeper {
public:TimeKeeper();~TimeKeeper();// ...
};class AtomicClock: public TimeKeeper {   };        // 原子钟
class WaterClock: public TimeKeeper {   };         // 水钟
class WristWatch: public TimeKeeper {   };         // 腕表

许多客户只想在程序中使用时间,不想操心时间如何计算等细节,这时候我们可以设计factory (工厂)函数,返回指针指向一个计时对象。Factory函数会“返回一个base class 指针,指向新生成之 derived class对象”:

TimeKeeper* getTimeKeeper();        // 返回一个指针,指向一个// TimeKeeper派生类动态分配的对象

为遵守factory函数的规矩,被getTimeKeeper()返回的对象必须位于heap。因此为了避免泄漏内存和其他资源,将factory函数返回的每一个对象适当地delete掉很重要:

TimeKeeper* ptk = getTimeKeeper();// ...delete ptk;

条款13说“倚赖客户执行delete动作,基本上便带有某种错误倾向”,条款18则谈到factory函数接口该如何修改以便预防常见之客户错误,但这些在此都是次要的,因为此条款内我们要对付的是上述代码的一个更根本弱点:纵使客户把每一件事都做对了,仍然没办法知道程序如何行动。

问题出在getTimeKeeper返回的指针指向一个derived class对象(例如AtomicClock),而那个对象却经由一个base class指针(例如一个TimeKeeper*指针)被删除,而目前的base class ( TimeKeeper)有个non-virtual析构函数。

这是一个引来灾难的秘诀,因为C++明白指出,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义———实际执行时通常发生的是对象的derived成分没被销毁。如果getTimeKeeper返回指针指向一个AtomicClock 对象,其内的 AtomicClock 成分(也就是声明于Atomicclock class内的成员变量)很可能没被销毁,而AtomicClock的析构函数也未能执行起来。然而其base class成分(也就是TimeKeeper这一部分)通常会被销毁,于是造成一个诡异的“局部销毁”对象。这可是形成资源泄漏、败坏之数据结构、在调试器上浪费许多时间的绝佳途径喔。

消除这个问题的做法很简单:给base class一个virtual析构函数。此后删除derive dclass对象就会如你想要的那般。是的,它会销毁整个对象,包括所有derived class成分:

class TimeKeeper {
public:TimeKeeper();virtual ~TimeKeeper();// ...
};TimeKeeper* ptk = getTimeKeeper();// ...delete ptk;

像TimeKeeper这样的base classes除了析构函数之外通常还有其他virtual函数,因为 virtual函数的目的是允许derived class 的实现得以客制化(见条款34)。例如TimeKeeper就可能拥有一个virtual getcurrentTime,它在不同的derived classes 中有不同的实现码。任何 class只要带有 virtual函数都几乎确定应该也有一个virtual析构函数。

如果class 不含virtual函数,通常表示它并不意图被用做一个base class。当class不企图被当作 base class,令其析构函数为virtual往往是个嫂主意。考虑一个用来表示二维空间点坐标的class:

馊主意???

class Point {
public:Point(int xCoord, int yCoord);~Point();private:int x, y;
};

如果int占用32 bits,那么Point对象可塞入一个64-bit缓存器中。更有甚者,这样一个Point对象可被当做一个“64-bit 量”传给以其他语言如C或FORTRAN撰写的函数。然而当Point的析构函数是virtual,形势起了变化。

欲实现出 virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常是由一个所谓vptr ( virtual table pointer)指针指出。 vptr指向一个由函数指针构成的数组,称为vtbl( virtual table);每一个带有 virtual函数的class 都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl—编译器在其中寻找适当的函数指针。

多了虚函数表

virtual函数的实现细节不重要。重要的是如果Point class内含virtual函数,其对象的体积会增加:在32-bit计算机体系结构中将占用64 bits (为了存放两个ints)至96 bits(两个ints加上 vptr);在64-bit计算机体系结构中可能占用64~128 bits,因为指针在这样的计算机结构中占64 bits。因此,为 Point添加一个vptr会增加其对象大小达50%~100%,Point对象不再能够塞入一个64-bit缓存器,而C++的Point对象也不再和其他语言(如C)内的相同声明有着一样的结构(因为其他语言的对应物并没有 vptr),因此也就不再可能把它传递至(或接受自)其他语言所写的函数,除非你明确补偿vptr—一那属于实现细节,也因此不再具有移植性。

因此,无端地将所有 classes的析构函数声明为virtual,就像从未声明它们为virtual一样,都是错误的。许多人的心得是:只有当class内含至少一个virtual函数,才为它声明virtual析构函数。

即使class完全不带virtual函数,被“non-virtual析构函数问题”给咬伤还是有可能的。举个例子,标准string不含任何virtual函数,但有时候程序员会错误地把它当做base class:

class SpecialString: public std::string {// 馊主意。std::string有个非虚的析构函数
};

乍看似乎无害,但如果你在程序任意某处无意间将一个pointer-to-SpecialString转换为一个pointer-to-string,然后将转换所得的那个string 指针 delete掉,你立刻被流放到“行为不明确”的恶地上:

将子类指针转换成父类指针,让后delete掉

Specialstring* pss = new Specialstring ("Impending Doom");
std::string* ps;ps = pss;        // Specialstring* =》 std::string*delete ps;//未有定义!现实中*ps的 Specialstring资源会泄漏,//因为SpecialString析构函数没被调用。

相同的分析适用于任何不带virtual析构函数的 class,包括所有STL容器如vector, list,set, tr1 : : unordered_map(见条款54)等等。如果你曾经企图继承一个标准容器或任何其他“带有non-virtual析构函数”的class,拒绝诱惑吧!(很不幸C+没有提供类似Java的final classes或C#的sealed classes那样的“禁止派生”机制。)

有时候令class带一个pure virtual析构函数,可能颇为便利。还记得吗, pure virtual函数导致abstract(抽象)classes -—也就是不能被实体化(instantiated)的class。也就是说,你不能为那种类型创建对象。然而有时候你希望拥有抽象class,但手上没有任何pure virtual函数,怎么办?唔,由于抽象class总是企图被当作一个base class来用,而又由于base class应该有个virtual析构函数,并且由于pure virtual函数会导致抽象class,因此解法很简单:为你希望它成为抽象的那个class声明一个pure virtual析构函数。下面是个例子:

class AWOV {
public:virtual ~AWOV() = 0;        // 纯析构函数};

这个class有一个pure virtual函数,所以它是个抽象class,又由于它有个virtual析构函数,所以你不需要担心析构函数的问题。然而这里有个窍门:你必须为这个pure virtual析构函数提供一份定义:

AwOV:: ~AWOV() { }            //pure virtual析构函数的定

第一次知道要这样。

析构函数的运作方式是,最深层派生( most derived)的那个class其析构函数最先被调用,然后是其每一个base class的析构函数被调用。编译器会在 AWov的derivedclasses的析构函数中创建一个对~AWOV的调用动作,所以你必须为这个函数提供一份定义。如果不这样做,连接器会发出抱怨。

“给base classes一个 virtual析构函数”,这个规则只适用于polymorphic(带多态性质的)base classes身上。这种base classes的设计目的是为了用来“通过base class接口处理derived class对象”。TimeKeeper就是一个polymorphic base class,因为我们希望处理AtomicClock和 waterClock对象,纵使我们只有TimeKeeper 指针指向它们。

并非所有base classes的设计目的都是为了多态用途。例如标准string和STL容器都不被设计作为base classes使用,更别提多态了。某些classes的设计目的是作为base classes 使用,但不是为了多态用途。这样的classes 如条款6的Uncopyable和标准程序库的input_iterator_tag(条款47),它们并非被设计用来“经由baseclass接口处置derived class对象”,因此它们不需要virtual析构函数。

请记住

  • polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何 virtual函数,它就应该拥有一个virtual析构函数。
  • Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数

相关文章:

Effective C++条款07——为多态基类声明virtual析构函数(构造/析构/赋值运算)

有许多种做法可以记录时间,因此,设计一个TimeKeeper base class和一些derived classes 作为不同的计时方法,相当合情合理: class TimeKeeper { public:TimeKeeper();~TimeKeeper();// ... };class AtomicClock: public TimeKeepe…...

用友Java后端笔试2023-8-5

计算被直线划分区域 在笛卡尔坐标系,存在区域[A,B],被不同线划分成多块小的区域,简单起见,假设这些不同线都直线并且不存在三条直线相交于一点的情况。 img 那么,如何快速计算某个时刻,在 X 坐标轴上[ A,…...

idea2023 springboot2.7.5+mybatis+jsp 初学单表增删改查

创建项目 因为2.7.14使用量较少,特更改spring-boot为2.7.5版本 配置端口号 打开Sm01Application类,右键运行启动项目,或者按照如下箭头启动 启动后,控制台提示如下信息表示成功 此刻在浏览器中输入:http://lo…...

大语言模型之四-LlaMA-2从模型到应用

最近开源大语言模型LlaMA-2火出圈,从huggingface的Open LLM Leaderboard开源大语言模型排行榜可以看到LlaMA-2还是非常有潜力的开源商用大语言模型之一,相比InstructGPT,LlaMA-2在数据质量、培训技术、能力评估、安全评估和责任发布方面进行了…...

Android 远程真机调研

背景 现有的安卓测试机器较少,很难满足 SDK 的兼容性测试及线上问题(特殊机型)验证,基于真机成本较高且数量较多的前提下,可以考虑使用云测平台上的机器进行验证,因此需要针对各云测平台进行调研、比较。 …...

B. 攻防演练 (2021CCPC女生赛)

题意: 给出一个长度为n的字符,字符是前m个小写字母,有q个询问,每次询问一个最短子序列的长度满足不是[l,r]内任意一个子序列 思路: [l,r]中子序列可以看成是从[l,r]中的某个位置开始,跳到下一个字符的位…...

MAC环境,在IDEA执行报错java: -source 1.5 中不支持 diamond 运算符

Error:(41, 51) java: -source 1.5 中不支持 diamond 运算符 (请使用 -source 7 或更高版本以启用 diamond 运算符) 进入设置 修改java版本 pom文件中加入 <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin&l…...

Tomcat日志中文乱码

修改安装目录下的日志配置 D:\ProgramFiles\apache-tomcat-9.0.78\conf\logging.properties java.util.logging.ConsoleHandler.encoding GBK...

最小生成树 — Prim算法

同Kruskal算法一样&#xff0c;Prim算法也是最小生成树的算法&#xff0c;但与Kruskal算法有较大的差别。 Prim算法整体是通过“解锁” “选中”的方式&#xff0c;点 -> 边 -> 点 -> 边。 因为是最小生成树&#xff0c;所以针对的也是无向图&#xff0c;所以可以随意…...

如何使用PHP Smarty模板进行AJAX交互?

首先&#xff0c;我们要明白&#xff0c;AJAX是一种在无需刷新整个页面的情况下&#xff0c;与服务器进行通信的技术。这对于改善用户体验来说&#xff0c;是个大宝贝。而PHP Smarty模板则是PHP的一种模板引擎&#xff0c;它使得设计和开发人员能够更好地分离逻辑和显示。 现在…...

nginx反向代理、负载均衡

修改nginx.conf的配置 upstream nginx_boot{# 30s内检查心跳发送两次包&#xff0c;未回复就代表该机器宕机&#xff0c;请求分发权重比为1:2server 192.168.87.143 weight100 max_fails2 fail_timeout30s; server 192.168.87.1 weight200 max_fails2 fail_timeout30s;# 这里的…...

React Native文本添加下划线

import { StyleSheet } from react-nativeconst styles StyleSheet.create({mExchangeCopyText: {fontWeight: bold, color: #1677ff, textDecorationLine: underline} })export default styles...

微服务-Nacos(配置管理)

配置更改热更新 在Nacos中添加配置信息&#xff1a; 在弹出表单中填写配置信息&#xff1a; 配置获取的步骤如下&#xff1a; 1.引入Nacos的配置管理客户端依赖&#xff08;A、B服务&#xff09;&#xff1a; <!--nacos的配置管理依赖--><dependency><groupId&…...

UML图绘制 -- 类图

1.类图的画法 类 整体是个矩形&#xff0c;第一层类名&#xff0c;第二层属性&#xff0c;第三层方法。 &#xff1a;public- : private# : protected空格: 默认的default 对应的类写法。 public class Student {public String name;public Integer age;protected I…...

SAP ME2L/ME2M/ME3M报表增强添加字段(包含:LMEREPI02、SE18:ES_BADI_ME_REPORTING)

ME2L、ME2M、ME3M这三个报表的字段增强&#xff0c;核心点都在同一个结构里 SE11:MEREP_OUTTAB_PURCHDOC 在这里加字段&#xff0c;如果要加的字段是EKKO、EKPO里的数据&#xff0c;直接加进去&#xff0c;啥都不用做&#xff0c;就完成了 如果要加的字段不在EKKO和EKPO这两个…...

探讨uniapp的数据缓存问题

异步就是不管保没保存成功&#xff0c;程序都会继续往下执行。同步是等保存成功了&#xff0c;才会执行下面的代码。使用异步&#xff0c;性能会更好&#xff1b;而使用同步&#xff0c;数据会更安全。 1 uni.setStorage(OBJECT) 将数据存储在本地缓存中指定的 key 中&#x…...

服务的拆分

纵向拆分 是从业务维度进行拆分。标准是按照业务的关联程度来决定&#xff0c;关联比较密切的业务适合拆分为一个微服务&#xff0c;而功能相对比较独立的业务适合单独拆分为一个微服务。 以社交App为例&#xff0c;你可以认为首页信息流是一个服务&#xff0c;评论是一个服务…...

Uniapp Syntax Error: Error: Unbalanced delimiter found in string

报错 in ./src/pages/user/components/tasks.vue?vue&typescript&langjs&Syntax Error: Error: Unbalanced delimiter found in string...这边导致文件的原因&#xff1a;可能是条件编译语法不小心删了某个字符&#xff0c;导致不全&#xff0c;无法形成一对。 //…...

视频集中存储EasyCVR视频汇聚平台定制项目增加AI智能算法

安防视频集中存储EasyCVR视频汇聚平台&#xff0c;可支持海量视频的轻量化接入与汇聚管理。平台能提供视频存储磁盘阵列、视频监控直播、视频轮播、视频录像、云存储、回放与检索、智能告警、服务器集群、语音对讲、云台控制、电子地图、平台级联、H.265自动转码等功能。为了便…...

确保Django项目的稳定运行和持续改进

确保Django项目的稳定运行和持续改进 引言 Django是一个强大的Python Web框架&#xff0c;用于构建高效、可靠的Web应用程序。然而&#xff0c;部署一个Django项目并不意味着工作已经完成。在项目上线之后&#xff0c;确保项目的稳定运行并不断进行改进是非常重要的。本博客将…...

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …...

docker详细操作--未完待续

docker介绍 docker官网: Docker&#xff1a;加速容器应用程序开发 harbor官网&#xff1a;Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台&#xff0c;用于将应用程序及其依赖项&#xff08;如库、运行时环…...

STM32F4基本定时器使用和原理详解

STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具

文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...

【2025年】解决Burpsuite抓不到https包的问题

环境&#xff1a;windows11 burpsuite:2025.5 在抓取https网站时&#xff0c;burpsuite抓取不到https数据包&#xff0c;只显示&#xff1a; 解决该问题只需如下三个步骤&#xff1a; 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...

反射获取方法和属性

Java反射获取方法 在Java中&#xff0c;反射&#xff08;Reflection&#xff09;是一种强大的机制&#xff0c;允许程序在运行时访问和操作类的内部属性和方法。通过反射&#xff0c;可以动态地创建对象、调用方法、改变属性值&#xff0c;这在很多Java框架中如Spring和Hiberna…...

大模型多显卡多服务器并行计算方法与实践指南

一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...

是否存在路径(FIFOBB算法)

题目描述 一个具有 n 个顶点e条边的无向图&#xff0c;该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序&#xff0c;确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数&#xff0c;分别表示n 和 e 的值&#xff08;1…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

2023赣州旅游投资集团

单选题 1.“不登高山&#xff0c;不知天之高也&#xff1b;不临深溪&#xff0c;不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...