More Effective C++----异常 & (9)使用析构函数防止资源泄漏

异常

关于C++异常的详细知识,请参考<http://blog.csdn.net/qianqin_2014/article/details/51325842>

C++新增的异常(exception)机制改变了某些事情,这种改变是深刻的,彻底的,可能是令人不舒服的。例如:使用未经处理的或原始的指针变得很危险;资源泄漏的可能性增加了;写出具有你希望的行为的构造函数与析构函数变得更加困难。特别小心防止程序执行时突然崩溃。执行程序和库程序尺寸增加了,同时运行速度降低了。

这就使我们所知道的事情。很多使用C++的人都不知道在程序中使用异常,大多数人不知道如何正确使用它。在异常被抛出后,使软件的行为具有可预测性和可靠性,在众多方法中至今也没有一个一致的方法能做到这点。(为了深刻了解这个问题,参见Tom Cargill写的Exception Handling: A False Sense of Security。有关这些问题的进展情况的信息,参见Jack Reeves 写的Coping with Exceptions和Herb
Sutter写的Exception-Safe Generic Containers。)

我们知道:程序能够在存在异常的情况下正常运行是因为它们按照要求进行了设计,而不是因为巧合。异常安全(Exception-safe)的程序不是偶然建立的。一个没有按照要求进行设计的程序在存在异常的情况下运行正常的概率与一个没有按照多线程要求进行设计的程序在多线程的环境下运行正常的概率相同,概率为0。

为什么使用异常呢?自从C语言被发明初来,C程序员就满足于使用错误代码(Error code),所以为什么还要弄来异常呢,特别是如果异常如我上面所说的那样存在着问题。答案是简单的:异常不能被忽略。如果一个函数通过设置一个状态变量或返回错误代码来表示一个异常状态,没有办法保证函数调用者将一定检测变量或测试错误代码。结果程序会从它遇到的异常状态继续运行,异常没有被捕获,程序立即会终止执行。

C程序员能够仅通过setjmp和longjmp来完成与异常处理相似的功能。但是当longjmp在C++中使用时,它存在一些缺陷,当它调整堆栈时不能对局部对象调用析构函数。(WQ加注,VC++能保证这一点,但不要依赖这一点。)而大多数C++程序员依赖于这些析构函数的调用,所以setjmp和longjmp不能够替换异常处理。如果你需要一个方法,能够通知不可被忽略的异常状态,并且搜索栈空间(searching the stack)以便找到异常处理代码时,你还得确保局部对象的析构函数必须被调用,这时你就需要使用C++的异常处理。

因为我们已经对使用异常处理的程序设计有了很多了解,下面这些条款仅是一个对于写出异常安全(Exception-safe)软件的不完整的指导。然而它们给任何在C++中使用异常处理的人介绍了一些重要思想。通过留意下面这些指导,你能够提高自己软件的正确性,强壮性和高效性,并且你将回避开许多在使用异常处理时经常遇到的问题。

Item M9:使用析构函数防止资源泄漏

对指针说再见。必须得承认:你永远都不会喜欢使用指针。

Ok,你不用对所有的指针说再见,但是你需要对用来操纵局部资源(local resources)的指针说再见。假设,你正在为一个小动物收容所编写软件,小动物收容所是一个帮助小狗小猫寻找主人的组织。每天收容所建立一个文件,包含当天它所管理的收容动物的资料信息,你的工作是写一个程序读出这些文件然后对每个收容动物进行适当的处理(appropriate
processing)。

完成这个程序一个合理的方法是定义一个抽象类,ALA("Adorable Little Animal"),然后为小狗和小猫建立派生类。一个虚拟函数processAdoption分别对各个种类的动物进行处理:

class ALA {
public:
  virtual void processAdoption() = 0;
  ...
};
class Puppy: public ALA {
public:
  virtual void processAdoption();
  ...
};
class Kitten: public ALA {
public:
  virtual void processAdoption();
  ...
};

你需要一个函数从文件中读取信息,然后根据文件中的信息产生一个puppy(小狗)对象或者kitten(小猫)对象。这个工作非常适合于虚拟构造器(virtual constructor),在条款M25详细描述了这种函数。为了完成我们的目标,我们这样声明函数:

// 从s中读去动物信息, 然后返回一个指针
// 指向新建立的某种类型对象
ALA * readALA(istream& s);

你的程序的关键部分就是这个函数,如下所示:

void processAdoptions(istream& dataSource)
{
  while (dataSource) {                  // 还有数据时,继续循环
    ALA *pa = readALA(dataSource);      //得到下一个动物
    pa->processAdoption();             //处理收容动物
    delete pa;                         //删除readALA返回的对象
  }
}

这个函数循环遍历dataSource内的信息,处理它所遇到的每个项目。唯一要记住的一点是在每次循环结尾处删除pa。这是必须的,因为每次调用readALA都建立一个堆对象。如果不删除对象,循环将产生资源泄漏。

现在考虑一下,如果pa->processAdoption抛出了一个异常,将会发生什么?processAdoptions没有捕获异常,所以,异常将传递给processAdoptions的调用者。传递中,processAdoptions函数中的调用pa->processAdoption语句后的所有语句都被跳过,这就是说pa没有被删除。结果,任何时候pa->processAdoption抛出一个异常都会导致processAdoptions内存泄漏。

堵塞泄漏很容易 :

void processAdoptions(istream& dataSource)
{
  while (dataSource) {
    ALA *pa = readALA(dataSource);
  try {
      pa->processAdoption();
  }
  catch (...) {              // 捕获所有异常
    delete pa;               // 避免内存泄漏
                             // 当异常抛出时
    throw;                   // 传送异常给调用者
  }
  delete pa;                 // 避免资源泄漏
}                           // 当没有异常抛出时
}

但是你必须用try和catch对你的代码进行小改动。更重要的是你必须写双份清除代码,一个为正常的运行准备,一个为异常发生时准备。在这种情况下,必须写两个delete代码。像其它重复代码一样,这种代码写起来令人心烦又难于维护,而且它看上去好像存在着问题。不论我们是让processAdoptions正常返回还是抛出异常,我们都需要删除pa,所以为什么我们必须要在多个地方编写删除代码呢?

(WQ加注,VC++支持try…catch…final结构的SEH。)

我们可以把总被执行的清除代码放入processAdoptions函数内的局部对象的析构函数里,这样可以避免重复书写清除代码。因为当函数返回时局部对象总是被释放,无论函数是如何退出的。(仅有一种例外就是当你调用longjmp时。Longjmp的这个缺点是C++率先支持异常处理的主要原因)

具体方法是用一个对象代替指针pa,这个对象的行为与指针相似。当pointer-like对象(类指针对象)被释放时,我们能让它的析构函数调用delete。替代指针的对象被称为smart pointers(灵巧指针)(C++ Primer中称为智能指针),参见条款M28的解释,你能使得pointer-like对象非常灵巧。在这里,我们用不着这么聪明的指针,我们只需要一个pointer-lik对象,当它离开生存空间时知道删除它指向的对象。

注:在C++ Primer 第五版中明确指出,auto_ptr虽然仍是标准库的一部分,但编写程序时应该使用unique_ptr。

写出这样一个类并不困难,但是我们不需要自己去写。标准C++库函数包含一个类模板,叫做auto_ptr,(为什么不适用shared_ptr或者unique_ptr呢?)这正是我们想要的。每一个auto_ptr类的构造函数里,让一个指针指向一个堆对象(heap object),并且在它的析构函数里删除这个对象。下面所示的是auto_ptr类的一些重要的部分:

template<class T>
class auto_ptr {
public:
  auto_ptr(T *p = 0): ptr(p) {}        // 保存ptr,指向对象
  ~auto_ptr() { delete ptr; }          // 删除ptr指向的对象
private:
  T *ptr;                              // raw ptr to object
};

auto_ptr类的完整代码是非常有趣的,上述简化的代码实现不能在实际中应用。(我们至少必须加上拷贝构造函数,赋值operator和将在条款M28讲述的pointer-emulating函数),但是它背后所蕴含的原理应该是清楚的:用auto_ptr对象代替raw指针,你将不再为堆对象不能被删除而担心,即使在抛出异常时,对象也能被及时删除。(因为auto_ptr的析构函数使用的是单对象形式的delete,所以auto_ptr不能用于指向对象数组的指针。如果想让auto_ptr类似于一个数组模板,你必须自己写一个。在这种情况下,用vector代替array可能更好。)

使用auto_ptr对象代替raw指针,processAdoptions如下所示:

void processAdoptions(istream& dataSource)
{
  while (dataSource) {
    auto_ptr<ALA> pa(readALA(dataSource));
    pa->processAdoption();
  }
}

这个版本的processAdoptions在两个方面区别于原来的processAdoptions函数。第一,pa被声明为一个auto_ptr<ALA>对象,而不是一个raw ALA*指针第二,在循环的结尾没有delete语句。其余部分都一样,因为除了析构的方式,auto_ptr对象的行为就象一个普通的指针。是不是很容易。

隐藏在auto_ptr后的思想是:用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源,这种思想不只是可以运用在指针上,还能用在其它资源的分配和释放上。想一下这样一个在GUI程序中的函数,它需要建立一个window来显式一些信息:

// 这个函数会发生资源泄漏,如果一个异常抛出
void displayInfo(const Information& info)
{
  WINDOW_HANDLE w(createWindow());
  //在w对应的window中显式信息
  destroyWindow(w);
}

很多window系统有C-like接口,使用象like createWindow 和destroyWindow函数来获取和释放window资源。如果在w对应的window中显示信息时,一个异常被抛出,w所对应的window将被丢失,就象其它动态分配的资源一样。

解决方法与前面所述的一样,建立一个类,让它的构造函数与析构函数来获取和释放资源:

class WindowHandle {
public:
   WindowHandle(WINDOW_HANDLE handle): w(handle) {}
  ~WindowHandle() { destroyWindow(w); }
   operator WINDOW_HANDLE() { return w; }        // see below
private:
  WINDOW_HANDLE w;
  // 下面的函数被声明为私有,防止建立多个WINDOW_HANDLE拷贝
  //有关一个更灵活的方法的讨论请参见条款M28。
  WindowHandle(const WindowHandle&);
  WindowHandle& operator=(const WindowHandle&);
};

这看上去有些象auto_ptr,只是赋值操作与拷贝构造被显式地禁止(参见条款M27),有一个隐含的转换操作能把WindowHandle转换为WINDOW_HANDLE。这个能力对于使用WindowHandle对象非常重要,因为这意味着你能在任何地方象使用raw WINDOW_HANDLE一样来使用WindowHandle。(参见条款M5 ,了解为什么你应该谨慎使用隐式类型转换操作)

通过给出的WindowHandle类,我们能够重写displayInfo函数,如下所示:

// 如果一个异常被抛出,这个函数能避免资源泄漏
void displayInfo(const Information& info)
{
  WindowHandle w(createWindow());
  在w对应的window中显式信息;
}

即使一个异常在displayInfo内被抛出,被createWindow 建立的window也能被释放。

资源应该被封装在一个对象里,遵循这个规则,你通常就能避免在存在异常环境里发生资源泄漏但是如果你正在分配资源时一个异常被抛出,会发生什么情况呢?例如当你正处于resource-acquiring类的构造函数中。还有如果这样的资源正在被释放时,一个异常被抛出,又会发生什么情况呢?构造函数和析构函数需要特殊的技术。你能在条款M10和条款M11中获取有关的知识。

总结:使用智能指针,并把资源释放函数放在析构函数中来防止内存泄漏!

时间: 2024-10-16 16:04:13

More Effective C++----异常 & (9)使用析构函数防止资源泄漏的相关文章

More Effective C++----(10)在构造函数中防止资源泄漏

Item M10:在构造函数中防止资源泄漏 如果你正在开发一个具有多媒体功能的通讯录程序.这个通讯录除了能存储通常的文字信息如姓名.地址.电话号码外,还能存储照片和声音(可以给出他们名字的正确发音). 为了实现这个通信录,你可以这样设计: class Image { // 用于图像数据 public: Image(const string& imageDataFileName); ... }; class AudioClip { // 用于声音数据 public: AudioClip(const

effective c++条款13-17 “以对象管理资源”之RAII浅析

RAII是指C++语言中的一个惯用法(idiom),它是"Resource Acquisition Is Initialization"的首字母缩写.中文可将其翻译为"资源获取就是初始化".虽然从某种程度上说这个名称并没有体现出该惯性法的本质精神,但是作为标准C++资源管理的关键技术,RAII早已在C++社群中深入人心. 使用局部对象管理资源的技术通常称为"资源获取就是初始化".这种通用技术依赖于构造函数和析构函数的性质以及它们与异常处理的交互作

MFC 错误异常,用vs添加资源并为资源定义类后报错:error C2065 : 未声明的标识符

添加了一个Dialog资源,修改了ID之后右击资源添加了一个类,在类里面有一个成员变量: // 对话框数据    enum { IDD = IDD_GETIN }; 而在编译过程中出现报错,错误代号是error C2065 : 未声明的标识符,我的第一反应是为什么我没通过手动添加资源而是通过VS添加都会出现这种情况呢,我想应该是其它地方错误导致此报错吧,但是却没想过,此类错误往往是因为没有包含某个头文件而引起的. 最后我是这样解决的:添加了一个#include"Resource.h"

effective c++条款13-17 “以对象管理资源”之auto_ptr源码分析

auto_ptr是当前C++标准库中提供的一种智能指针,诚然,auto_ptr有这样那样的不如人意,以至于程序员必须像使用"裸"指针那样非常小心的使用它才能保证不出错,以至于它甚至无法适用于同是标准库中的那么多的容器和一些算法,但即使如此,我们仍然不能否认这个小小的auto_ptr所蕴含的价值与理念. 这里用了Nicolai M. Josuttis(<<The C++ standard library>>作者)写的一个auto_ptr的版本,并做了少许格式上的修

c++ 虚析构函数[避免内存泄漏]

c++  虚析构函数: 虚析构函数(1)虚析构函数即:定义声明析构函数前加virtual 修饰, 如果将基类的析构函数声明为虚析构函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚析构函数. (2)基类指针pbase 指向用new动态创建的派生类对象child时,用“delete pbase;”删除对象分两种情况:第一,如果基类中的析构函数为虚析构函数,则会先删除派生类对象,再删除基类对象第二,如果基类中的析构函数为非虚析构函数,则只会删除基类对象,不会删除派生类对象,这样便出现了内存泄

effective c++条款13-17 “以对象管理资源”之C++隐式转换和转换构造函数

其实我们已经在C/C++中见到过多次标准类型数据间的转换方式了,这种形式用于在程序中将一种指定的数据转换成另一指定的类型,也即是强制转换,比如:int a = int(1.23),其作用是将1.23转换为整形1.然而对于用户自定义的类类型,编译系统并不知道如何进行转换,所以需要定义专门的函数来告诉编译系统改如何转换,这就是转换构造函数和类型转换函数! 注意:转换构造函数.隐式转换和函数对象不要搞混淆!!!函数对象是重载运算符(),和隐式转换函数易混淆. 一.转换构造函数 转换构造函数(conve

effective c++条款13-17 “以对象管理资源”之shared_ptr浅析

顾名思义,boost::shared_ptr是可以共享所有权的智能指针,首先让我们通过一个例子看看它的基本用法: #include <string> #include <iostream> #include <boost/shared_ptr.hpp> class implementation { public: ~implementation() { std::cout <<"destroying implementation\n";

C++处理异常 try,catch,throw,finally

异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制. 也许我们已经使用过异常,但是你会是一种习惯吗,不要老是想着当我打开一个文件的时候才用异常判断一下,我知道对你来说你喜欢用return value或者是print error message来做,你想过这样做会导致Memory Leak,系统退出,代码重复/难读,垃圾一堆…..吗?现在的软件已经是n*365*24小时的运行了,软件的健壮已经是一个很要考虑的时候了.自序:对写程序来说异常真的是很重要,一个稳健的代码不是靠返回

More Effective C++

条款一:指针与引用的区别 指针与引用看上去完全不同(指针用操作符'*'和'->',引用使用操作符'.'),但是它们似乎有相同的功能.指针与引用都是让你间接引用其他对象.你如何决定在什么时候使用指针,在什么时候使用引用呢? 首先,要认识到在任何情况下都不能用指向空值的引用.一个引用必须总是指向某些对象.因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量.相反,如果变量肯定指向一个对象,例如你的设计不允许变量为