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算法一样,Prim算法也是最小生成树的算法,但与Kruskal算法有较大的差别。 Prim算法整体是通过“解锁” “选中”的方式,点 -> 边 -> 点 -> 边。 因为是最小生成树,所以针对的也是无向图,所以可以随意…...
如何使用PHP Smarty模板进行AJAX交互?
首先,我们要明白,AJAX是一种在无需刷新整个页面的情况下,与服务器进行通信的技术。这对于改善用户体验来说,是个大宝贝。而PHP Smarty模板则是PHP的一种模板引擎,它使得设计和开发人员能够更好地分离逻辑和显示。 现在…...
nginx反向代理、负载均衡
修改nginx.conf的配置 upstream nginx_boot{# 30s内检查心跳发送两次包,未回复就代表该机器宕机,请求分发权重比为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中添加配置信息: 在弹出表单中填写配置信息: 配置获取的步骤如下: 1.引入Nacos的配置管理客户端依赖(A、B服务): <!--nacos的配置管理依赖--><dependency><groupId&…...
UML图绘制 -- 类图
1.类图的画法 类 整体是个矩形,第一层类名,第二层属性,第三层方法。 :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这三个报表的字段增强,核心点都在同一个结构里 SE11:MEREP_OUTTAB_PURCHDOC 在这里加字段,如果要加的字段是EKKO、EKPO里的数据,直接加进去,啥都不用做,就完成了 如果要加的字段不在EKKO和EKPO这两个…...
探讨uniapp的数据缓存问题
异步就是不管保没保存成功,程序都会继续往下执行。同步是等保存成功了,才会执行下面的代码。使用异步,性能会更好;而使用同步,数据会更安全。 1 uni.setStorage(OBJECT) 将数据存储在本地缓存中指定的 key 中&#x…...
服务的拆分
纵向拆分 是从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。 以社交App为例,你可以认为首页信息流是一个服务,评论是一个服务…...
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...这边导致文件的原因:可能是条件编译语法不小心删了某个字符,导致不全,无法形成一对。 //…...
视频集中存储EasyCVR视频汇聚平台定制项目增加AI智能算法
安防视频集中存储EasyCVR视频汇聚平台,可支持海量视频的轻量化接入与汇聚管理。平台能提供视频存储磁盘阵列、视频监控直播、视频轮播、视频录像、云存储、回放与检索、智能告警、服务器集群、语音对讲、云台控制、电子地图、平台级联、H.265自动转码等功能。为了便…...
确保Django项目的稳定运行和持续改进
确保Django项目的稳定运行和持续改进 引言 Django是一个强大的Python Web框架,用于构建高效、可靠的Web应用程序。然而,部署一个Django项目并不意味着工作已经完成。在项目上线之后,确保项目的稳定运行并不断进行改进是非常重要的。本博客将…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...
SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现
摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序,以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务,提供稳定高效的数据处理与业务逻辑支持;利用 uniapp 实现跨平台前…...
C# 类和继承(抽象类)
抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...
uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...
华硕a豆14 Air香氛版,美学与科技的馨香融合
在快节奏的现代生活中,我们渴望一个能激发创想、愉悦感官的工作与生活伙伴,它不仅是冰冷的科技工具,更能触动我们内心深处的细腻情感。正是在这样的期许下,华硕a豆14 Air香氛版翩然而至,它以一种前所未有的方式&#x…...
【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...
stm32wle5 lpuart DMA数据不接收
配置波特率9600时,需要使用外部低速晶振...
MySQL的pymysql操作
本章是MySQL的最后一章,MySQL到此完结,下一站Hadoop!!! 这章很简单,完整代码在最后,详细讲解之前python课程里面也有,感兴趣的可以往前找一下 一、查询操作 我们需要打开pycharm …...
【WebSocket】SpringBoot项目中使用WebSocket
1. 导入坐标 如果springboot父工程没有加入websocket的起步依赖,添加它的坐标的时候需要带上版本号。 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dep…...
命令行关闭Windows防火墙
命令行关闭Windows防火墙 引言一、防火墙:被低估的"智能安检员"二、优先尝试!90%问题无需关闭防火墙方案1:程序白名单(解决软件误拦截)方案2:开放特定端口(解决网游/开发端口不通)三、命令行极速关闭方案方法一:PowerShell(推荐Win10/11)方法二:CMD命令…...
