条款10:在constructors 内阻止资源泄漏(resource leak)

想象你正在开发一个多媒体通信簿软件。这个软件可以放置包括人名、地址、电话号码等文字,以及一张个人相片和一段个人声音(或许是其姓名的发音)。
为实现此软件,你可能设计如下:

class Image {//给影像数据使用。
public:Image(const string& imageDataFileName);//...
};class AudioClip {
public://给音频数据使用。AudioClip(const string& audioDataFileName);//...
};class PhoneNumber { ... };// 用来放置电话号码。class BookEntry {//用来放置通信簿的每一个个人数据。
public:BookEntry(const string& name,const string& address = "",const string& imageFileName,const stringe audioclipFileName = "");~BookEntry();// 电话号码通过此函数加入。void addPhoneNumber(const PhoneNumber& number);
private:string theName;	// 个人姓名。string theAddress;	//个人地址。list<PhoneNumber> thePhones;//个人电话号码。Image* theImage;//个人相片。AudioClip* theAudioclip;//一段个人声音。};

每一个BookEntry 都必须有姓名数据,所以它必须成为一个constructor 自变量(见条款3),但是其他字段一—个人地址及相片文件和声音文件——都可有可无。注意,我利用1ist class放置个人电话号码,list是C++标准程序库(见条款E49和条款35)提供的数个容器类(container classes)之一。
    BookEntry constructor 和 destructor 可以直截了当地这么设计:

BookEntry::BookEntry(const string& name,const strings address,const string& imageFileName,const string& audioClipFileName):theName(name), theAddress(address),theImage(0), theAudioclip(0)
{if (imageFileName != ""){theImage = new Image(imageFileName);}if (audioclipFileName != "") {theAudioclip = new Audioclip(audioClipFileName);}
}
BookEntry::~BookEntry()
{delete theImage;delete theAudioClipi
}

其中constructor 先将指针 theImage 和theAudioClip 初始化为null;

如果对应的自变量不是空字符串,再让它们指向真正的对象。

destructor 负责删除上述两个指针,确保 BookEntry object 不会造成资源泄漏问题。由于C++保证“删除null指针”是安全的,所以BookEntry destructor 不必在删除指针之前先检查它们是否真正指向某些东西。

每件事看起来都很好,正常情况下每件事也的确很好,但是在不正常的情况下——在exception 出现的情况下——事情一点也不好。

当程序执行 BookEntry constructor 的以下部分,如果有个 exception 被抛出,会发生什么事?

if (audioclipFileName != "") 
{theAudioClip = new AudioClip(audioclipFileName);
}

exception的发生可能是由于 operator new(见条款8)无法分配足够的内存给一个 Audioclip object 使用,也可能是因为AudioClip constructor 本身抛出一个exception。

不论原因为何,只要是在 BookEntry constructor 内抛出,就会被传播到正在产生 BookEntryobject的那一端。

现在,如果在产生“原本准备让 theAudioclip指向”的对象时,发生了一个exception,控制权因而移出 BookEntry constructor 之外,谁来删除theImage 已经指向的那个对象呢?

明显的答案是由 BookEntry destructor来执行,但是这个明显的答案是个错误答案。BookEntry的destructor 绝不会被调用,绝对不会。

C++只会析构已构造完成的对象。对象只有在其constructor 执行完毕才算是完全构造妥当。所以如果程序打算产生一个局部性的 BookEntry object b:

void testBookEntryClass()
{BookEntry b("Addison-Wesley Publishing Company","One Jacob Way, Reading, MA 01867");
//...
}

而exception在b的构造过程中被抛出,b的destructor就不会被调用。如果你尝试更深入地参与,将b分配于heap 中,并在exception 出现时调用delete:

void testBookEntryClass()//注意这是类外
{
BookEntry* pb = 0;
try
{pb = new BookEntry("Addison-Wesley Publishing Company""One Jacob Way, Reading, MA 01867");
}
catch (...) {//捕提所有的exceptions。delete pb;//当exception 被抛出,删除pb。throw;// 将exception 传给调用者。
}	delete pb;//正常情况下删除pb。
}

你会发现BookEntry constructor 所分配的 Image object 还是泄漏了。因为除非new动作成功,否则上述那个 assignment(赋值)动作并不会施加于pb身上。

如果 BookEntry constructor 抛出一个 exception,pb 将成为null指针,此时在catch 语句块中删除它,除了让你感觉比较爽之外,别无其他作用。

以smart pointer class autoptr<BookEntry>(见条款9)取代原始的 BookEntry*,也不会让情况好转,因为除非new动作成功,否则对pb的赋值动作还是不会进行。

面对尚未完全构造好的对象,为什么C++拒绝调用其destructor呢?它可不是为了让你痛苦而做成这样的设计的。

是的,这是有理由的。如果那么做,许多时候会是一件没有意义的事,甚至是一件有害的事。如果destructor 被调用于一个尚末完全构造好的对象身上,这个destructor 如何知道该做些什么事呢?它唯一能够知道的机会就是:被加到对象内的那些数据身上附带有某种指示,指示constructor 进行到什么程度。那么destructor就可以检查这些数据并(或许能够)理解应该如何应对。如此繁重的簿记工作会降低constructors的速度,使每一个对象变得更庞大。C++避免这样的额外开销,但你必须付出“仅部分构造完成”的对象不会被自动销毁的代价(条款E13有另一个“效率与程序行为”之间的类似取舍决定)。

由于C++不自动清理那些“构造期间抛出exceptions”的对象,所以你必须设计你的constructors,使它们在那种情况下亦能自我清理。通常这只需将所有可能的exceptions 捕捉起来,执行某种清理工作,然后重新抛出exception,使它继续传播出去即可。这个策略可以这样纳入 BookEntry constructors

BookEntry::BookEntry(const string& name,const string& address,const string& imageFileName,const string& audioClipFileName)theName(name), theAddress(address),theImage(0), theAudioClip(0)
{try {// 这个try 语句块是新的。if (imageFileName != ""){theImage = new Image(imageFileName);}if (audioClipFileName != ""){theAudioClip = new AudioClip(audioClipFileName);}}catch (...)	//捕捉所有的exception。{delete theImage; //执行必要的清理工作。delete theAudioClip;throw;// 继续传播这个 exception。}}

不需要担心 BookEntry 的non-pointer data members。

Data members 会在class constructor 被调用之前就先初始化好(译注:因为此处使用了member initialization list,成员初值链表),所以当BookEntry constructor 函数本体开始执行,该对象的theName,theAddress 和 thePhones 等 data members 都已完全构造好了。

所以当BookEntry object 被销毁,其所内含的这此data members 就像“构造完全的对象”一样,也会被自动销毁,无须你插手。

当然啦,如果这些对象的constructors调用其他函数,而那些函数可能抛出exceptions,那么这些constructors 就必须负责捕捉exceptions,并在继续传播它们之前先执行任何必要的清理工作。

你可能已经注意到,BookEntry的catch 语句块内的动作和BookEntry的destructor内的动作相同。我们一向不遗余力地希望消除重复代码,这里也是一样的,所以最好是把共享代码抽出放进一个private 辅助函数内,然后让constructor 和destructor 都调用它:
 

class BookEntry {
public://与前同。
private:void cleanup()//共同的清理(clean up)动作放在这里};void BookEntry::cleanup()
{delete theImage;delete theAudioclip;
}BookEntry::BookEntry(const string& name,const strings address,const string& imageFileName,const string& audioclipFileName):theName(name), theAddress(address),theImage(0), theAudioclip(0){try {// 与前同。}catch (...){cleanup();//释放资源。throw;//传播 exception。}}BookEntry::~BookEntry(){cleanup()//释放资源。}
}:

好极了,但是本题并未就此结束。让我们稍加变化,让 theImage和theAudioClip 都变成常量指针:

class BookEntry
{
public://与前同。
private://这些指针都是const。Image*const theImage;Audioclip* const theAudioclip;
};

这样的指针必须通过 BookEntry constructors 的成员初值链表(member initialization lists)加以初始化,因为再没有其他方法可以给予const 指针一个值(见条款E12)。

一个常见的做法就是像下面这样给予theImage 和 theAudioClip 初值:

//注意,以下做法在发生 exception 时会导致资源泄漏
BookEntry::BookEntry(const string& name,const string& address,const string& imageFileName,const string& audioClipFileName):theName(name), theAddress(address),theImage(imageFilename != ""?new Image(imageFileName):0),theAudioClip(audioclipFileName != ""?new Audioclip(audioClipFileName):0)
{}

但这却导致我们最初极力想消除的问题:如果在theAudioc1ip初始化期间发生exception, theImage 所指对象并不会被销毁。

此外,我们也无法借此在constructor内加上try/catch 语句块来解决此问题,因为try和catch都是语句(statements),而member initialization lists只接受表达式(expressions)。这就是为什么我们必须使用?:操作符取代 if-then-else 语法来为theImage和theAudioClip设定初值的原因。

尽管如此,欲在“exceptions 传播至constructor 外”之前执行清理工作,唯一的机会就是捕捉那些exceptions.

所以既然我们无法将try和catch放到一个member initialization list 之中,势必得将它们放到其他某处。
一个可能的地点就是放到某些private member functions内,让 theImage 和 theAudioClip 在其中获得初值:

class BookEntry {
public:
private://与前同。// data members 与前同。Image* initImage(const strings imageFileName);AudioClip* initAudioClip(const string& audioClipFileName)
};BookEntry::BookEntry(const string& name,const string& address, const string& imageFileName, const string& audioclipFileName):theName(name),theAddress(address),theImage(initImage(imageFileName)),theAudioClip(initAudioClip(audioClipFileName)){}// theImage 首先被初始化,所以即使初始化失败亦无须担心// 资源泄漏问题。因此本函数不必处理任何exceptions。Image* BookEntry::initImage(const strings imagerileName){if (imageFileName)return new Image(imageFileName);elsereturn 0;}// theAudioClip第二个被初始化,所以如果在它初始化期间有// exception 被抛出,它必须确定将theImage 的资源释放掉。//这就是为什么本函数使用try...catch 的原因。AudioClip* BookEntry::initAudioclip(const string&audioClipFileName){try {if (audioClipFileName != "")return new AudioClip(audioClipFileName);elsereturn 0;}catch (...){delete theImage;throw;}}

这是个完美的结局,它解决了使我们左支右绌疲于奔命的问题。

缺点是:概念上应该由constructor 完成的动作现在却散布于数个函数中,造成维护上的困扰。

一个更好的解答是,接受条款9的忠告,将theImage和theAudioClip所指对象视为资源,交给局部对象来管理。这个办法立足所依据的事实是,不论theImage 和theAudioClip都是指向动态分配而得的对象,当指针本身停止活动,那些对象都应该被删除。这正是auto_ptr class(见条款9)的设计目的。所以我们可以将 theImage 和 theAudioclip 的原始指针类型改为 auto_ptr:

class BookEntry {
public:// 与前同。
private:const auto_ptr<Image> theImage;// 注意,改用const auto_ptr<Audioclip> theAudioclip;// auto _ptr对象。
};

这么做便可以让BookEntry constructor在异常出现时免于资源泄漏的恐惧,也让我们得以利用member initialization list 将theImage和theAudioclip初始化:

BookEntry::BookEntry(const strings name,const string& address,const string& imageFileName,const string& audioClipFileName):theName(name), theAddress(address),theImage(imageFileName != ""? new Image(imageFileName):0),theAudioClip(audioClipFileName !=""? new Audioclip(audioClipFileName):0)
{}

在此设计中,如果 theAudioc1ip 初始化期间有任何 exception 被抛出:theImage已经是完整构造好的对象,所以它会被自动销毁,就像theName,theAddress 和thePhones 一样。

此外,由于 theImage 和 theAudioClip如今都是对象,当其“宿主”BookEntry被销毁,它们亦将被自动销毁。因此不再需要以手动方式删除它们所指的对象。这会大幅简化 BookEntry destructor:

BookEntry;:~BookEntry()()
//不需要做什么事!

意味你可以完全摆脱 BookEntry destructor。

结论是:如果你以auto_ptr 对象来取代pointer class members,你便对你的constructors 做了强化工事,免除了“exceptions 出现时发生资源泄漏”的危机,不再需要在destructors 内亲自动手释放资源,并允许const member pointers得以和non-const member pointers 有着一样优雅的处理方式。

处理“构造过程中可能发生的exceptions”,相当棘手。但是auto_ptr(以及与auto ptr相似的classes)可以消除大部分劳役。使用它们,不仅能够让代码更容易理解,也使程序在面对exceptions时更健壮。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/17552.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

升级 JDK17 一个不可拒绝的理由!

插&#xff1a; AI时代&#xff0c;程序员或多或少要了解些人工智能&#xff0c;前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家(前言 – 人工智能教程 ) 坚持不懈&#xff0c;越努力越幸运&#xff0c;大家…

Spring AOP基于动态代理的实现的 AOP

目录 代理什么是代理代理模式 静态代理动态代理JDK动态代理CGLIB动态代理Spring AOP使用的是哪种代理&#xff1f; 代理 什么是代理 生活中的代理 房产中介 &#xff1a; 房屋进行租赁时&#xff0c;卖方会把房子授权给中介&#xff0c;由中介代理带客户看房&#xff0c;商谈…

设计师可以学什么程序编程

设计师可以学什么程序编程 在数字化日益发展的今天&#xff0c;设计师们不仅需要具备出色的创意和设计能力&#xff0c;同时掌握一定的程序编程技能也变得越来越重要。这不仅可以帮助设计师更好地将创意转化为实际产品&#xff0c;还能提高工作效率&#xff0c;拓宽职业发展空…

枚举(C语言)

1.枚举定义 枚举是 C 语言中的一种基本数据类型&#xff0c;用于定义一组具有离散值的常量&#xff0c;它可以让数据更简洁&#xff0c;更易读。 枚举类型通常用于为程序中的一组相关的常量取名字&#xff0c;以便于程序的可读性和维护性。 定义一个枚举类型&#xff0c;需要使…

行为型设计模式之模板模式

文章目录 概述原理结构图实现 小结 概述 模板方法模式(template method pattern)原始定义是&#xff1a;在操作中定义算法的框架&#xff0c;将一些步骤推迟到子类中。模板方法让子类在不改变算法结构的情况下重新定义算法的某些步骤。 模板方法中的算法可以理解为广义上的业…

Function Calling学习

Function Calling第一篇 Agent&#xff1a;AI 主动提要求Function Calling&#xff1a;AI 要求执行某个函数场景举例&#xff1a;明天上班是否要带伞&#xff1f;AI反过来问你&#xff0c;明天天气怎么样&#xff1f; Function Calling 的基本流程 Function Calling 完整的官…

北斗高精度定位终端的工作原理和精度范围

北斗高精度定位终端的工作原理主要基于北斗卫星导航系统&#xff0c;通过卫星信号的接收、处理和计算&#xff0c;实现了对目标位置的精确测量。以下是关于北斗高精度定位终端工作原理的引文&#xff1a; ​ 北斗高精度定位终端作为一款新型的高精定位设备&#xff0c;其核心…

使用uniapp内置组件checkbox-group所遇到的问题

checkbox-group属性说明 属性名类型默认值说明changeEventHandle <checkbox-group> 中选项发生改变触发change事件 detail { value&#xff1a;[选中的checkbox的value的数组] } 问题代码 <checkbox-group change"handleEVent()"><view style&qu…

python2.7安装M2Crypto

对于Python 2.7安装M2Crypto&#xff0c;你可以按照以下步骤进行&#xff1a; 环境准备 操作系统&#xff1a;根据你的操作系统&#xff08;如macOS、Windows等&#xff09;&#xff0c;你需要确保你的Python 2.7环境已经正确设置。 依赖库&#xff1a; SWIG&#xff1a;M2Cry…

pg_lakehouse 与 datafusion

原理分析 pg_lakehouse 是 ParadeDB 推出的一个开源插件&#xff0c;支持对多种数据湖里的数据做分析计算。它的出现&#xff0c;使得 Postgres 能够像访问本地数据一样轻松访问 S3 等对象存储&#xff0c;轻松访问 Delta Lake 上的表格&#xff0c;具备数据湖分析能力。 pg_…

微信小程序实现容器图片流式布局功能,配合小程序原生框架使用。

小程序实现容器图片流式布局功能&#xff0c;因为目前论坛上也有很多博主出过类似的文章&#xff0c;这里我就以一个小白角度去讲一下如何实现的吧。给作者一点点鼓励&#xff0c;先点个赞赞吧&#x1f44d;&#xff0c;蟹蟹&#xff01;&#xff01; 目标 实现下方效果图 技术…

sonar3 使用 api/measures/componet 获取代码当,Java实现

最近团队在做一个技术架构相关的优化&#xff0c;当前的目标是想要通过代码量&#xff0c;系统架构入手。先统计到部门的代码量&#xff0c;如何进行代码行数的统计呢&#xff0c;因为我们采用的是Java技术栈&#xff0c;我就Java技术栈进行说明。 1、如何统计代码行数 要统计…

ZYNQ AXI4 FDMA内存读写

1 概述 如果用过ZYNQ的都知道,要直接操作PS的DDR 通常是DMA 或者VDMA,然而用过XILINX 的DMA IP 和 VDMA IP,总有一种遗憾,那就是不够灵活,还需要对寄存器配置,真是麻烦。对于我们搞 FPGA 的人来说,最喜欢直接了当,直接用FPGA代码搞定。现在XILINX 的总线接口是AXI4总线…

C-数据结构-树转存广义表-广义表转成树-实例

树转存广义表 save.c #include<stdio.h> #include<stdlib.h>#define FNAME "/tmp/out"struct node_st {char data;struct node_st *l,*r; };static struct node_st *tree NULL;//把tree提升到全局变量,当前文件int insert(struct node_st **root,int d…

latex中复制到word里面之后如何转变成word自带的公式

详细步骤如下&#xff1a; 第一步&#xff0c;将latex中的公式复制到word里面&#xff0c;例如&#xff1a;$r_1^d$ 第二步&#xff0c;选中$$里面的部分&#xff0c;也就是去掉$$&#xff0c;选中剩余的部分&#xff0c;例如&#xff1a;r_1^d 第三步&#xff0c;word工具栏里…

Web前端三大主流框架深度解析:React、Angular与Vue的较量

在现代Web开发中&#xff0c;前端框架已经成为开发人员的标准工具。它们不仅提供了丰富的功能&#xff0c;极大地简化了复杂的应用开发过程&#xff0c;还能提高开发效率和代码的可维护性。目前&#xff0c;React、Vue和Angular被认为是Web前端开发的三大主流框架。本文将深入探…

【Javascript修炼篇】你一天会犯几次低级错误

最近&#xff0c;尝试出一个javascript修炼篇&#xff0c;让编程技术更上一层楼。如果你对Javascript有兴趣&#xff0c;或者想要提供自己的编程技术&#xff0c;那么这个系列就很适合你。欢迎关注&#xff0c;持续更新中… 新手&#xff1a;作为人类&#xff0c;犯错实在太常…

echarts地图下钻+地图遮盖物散点

一、下载工具 npm i echarts echarts-gl axios -S -S是生产依赖默认是-S不写也可以 -D是开发依赖 二、引入工具 import * as echarts from "echarts"; import "echarts-gl"; import axios from "axios"; 三、HTML部分代码 <div class&…

【代码随想录】【算法训练营】【第21天】 [530]二叉搜索树的最小绝对差 [501]二叉搜索树的众数 [236]二叉树的最近公共祖先

前言 思路及算法思维&#xff0c;指路 代码随想录。 题目来自 LeetCode。 day 21&#xff0c;天气不错的周二~ 题目详情 [530] 二叉搜索树的最小绝对差 题目描述 530 二叉搜索树的最小绝对差 解题思路 前提&#xff1a;二叉搜索树 思路&#xff1a;根据二叉搜索树的中…

长安链使用Golang编写智能合约教程(二)

本篇说的是长安链2.3.的版本的智能合约&#xff0c;虽然不知道两者有什么区别&#xff0c;但是编译器区分。 教程三会写一些&#xff0c;其他比较常用SDK方法的解释和使用方法 编写前的注意事项&#xff1a; 1、运行一条带有Doker_GoVM的链 2、建议直接用官方的在线IDE去写合…