More Effective C++学习笔记(3)-异常

2017-01-14 08:44:21来源:CSDN作者:shaozhenged人点击

第七城市
主题 概要
C++ More Effective C++ 异常
编辑 时间
新建 20170113
序号 参考资料
1 More effective C++

C++中引进了异常机制,对比C语言程序员用错误码来标识错误,无疑更加优雅和健状。对于写出异常安全的代码,应该牢记遵循下面的准则。

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

对指针说再见。不用对所有的指针说再见,但是你需要对用来操纵局部资源(local resources)的指针说再见。
直接看代码:

// 从s中读去动物信息, 然后返回一个指针// 指向新建立的某种类型对象ALA * readALA(istream& s);//你的程序的关键部分就是这个函数void processAdoptions(istream& dataSource){    while (dataSource) { // 还有数据时,继续循环        ALA *pa = readALA(dataSource); //得到下一个动物        pa->processAdoption(); //处理收容动物        delete pa; //删除readALA返回的对象    }}

可以不用理解这个函数的具体作用,只要认识到这一点:while循环体内定义了一个局部指针变量,每次循环完后删除这个对象。问题出现在:

pa->processAdoption(); //处理收容动物delete pa; //删除readALA返回的对象

这两句代码之间。想像一下,当处理函数内部出现异常并抛出,则它后面的语句
delete pa
不会得到执行,则会出现资源泄漏的风险。

可能想到的法子是,直接在函数内部捕获这个异常:

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

但这里为了删除资源,要在两个地方维护,应当消除这种低质量的重复代码。

一点思考:能否依懒于 try…catch…final 块?

消除方法是把释放资源的操作放在函数体内局部对象的析构函数里,因为当函数返回时局部对象总是被释放,无论函数是如何退出的。具体方法是用一个对象代替指针pa,这个对象的行为与指针相似。当pointer-like对象(类指针对象)被释放时,我们能让它的析构函数调用delete。

C++里面和boost库里面就有这种对象,称为智能指针。

简易版的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};

现在就可以放心大胆的把上面的代码重写成这样:

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

这里就不存在堆对象里面资源不被释放的情况,因为即使发生异常,局部对象pa的析构函数总是会被调用。

Item M10:在构造函数中防止资源泄漏

这个条款前面部分来回看了几遍,但知道了结局真是差点眼泪掉下来。

还是看书中的例子:

这是一个具有多媒体功能的通讯录程序。这个通讯录除了能存储通常的文字信息如姓名、地址、电话号码外,还能存储照片和声音。简化一下,假设只能存放姓名,照片,和声音,则这个类可以这样设计:

class Image                 // 用于图像数据{ public:    Image(const string& imageDataFileName); };class AudioClip             // 用于声音数据{ public:    AudioClip(const string& audioDataFileName); };class BookEntry             // 通讯录中的条目{ public:    BookEntry(const string& name,const string& imageFileName = "",const string& audioClipFileName = "");    ~BookEntry();  private:    string theName;         // 人的姓名     Image *theImage;        // 他们的图像    AudioClip *theAudioClip; // 他们的一段声音片段};

这声明部分应该比较好理解,BookEntry是这个通讯录条目类,构造函数带有参数,对姓名、图像、和声音进行初始化。

下面看它的构造函数和析构函数,并分析其中可能出现的问题。

BookEntry::BookEntry(const string& name,const string& imageFileName,Const string& audioClipFileName)    : theName(name), theImage(0), theAudioClip(0){    if (imageFileName != "") {        theImage = new Image(imageFileName);    }    if (audioClipFileName != "") {        theAudioClip = new AudioClip(audioClipFileName);    }}BookEntry::~BookEntry(){    delete theImage;    delete theAudioClip;}

在正常情况下一切安好,但当出现异常时,就存在内存泄露的风险。
试想一下,

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

当在进行声音初始化时,发生了异常,可能是new 操作(申请内存),也可能是声音类的构造函数时发生了异常。那这个异常会被抛出,并且会传递到BookEntry构造函数的外部,那现在没有对这个异常进行捕捉,谁来负责删除已经建立好的theImage指向的对象?可能想的是析构函数来完成,但~BookEntry根本不会被调用。因为C++仅能删除被完全构造的对象,只有一个对象的构造函数完全运行完毕,这个对象才被完全地构造。

一点思考:我们自己定义类的构造函数,完成的功能应该绝对的简单,不应该主动抛出异常。

可能会想到在调用构造函数时主动的捕获这个异常并做处理:

void testBookEntryClass(){    BookEntry *pb = 0;    try     {        pb = new BookEntry("Addison-Wesley Publishing Company", "One Jacob Way, Reading, MA 01867");            }    catch (...)     // 捕获所有异常    {         delete pb; // 删除pb,当抛出异常时        throw; // 传递异常给调用者    }    delete pb; // 正常删除pb}

你会发现在BookEntry构造函数里为Image分配的内存仍旧被丢失了,这是因为如果new操作没有成功完成,程序不会对pb进行赋值操作。如果BookEntry的构造函数抛出一个异常,pb将是一个空值,所以在catch块中删除它除了让你自己感觉良好以外没有任何作用。

那更为主动,在构造函数中处理异常:

BookEntry::BookEntry(const string& name,const string& imageFileName,const string& audioClipFileName)    : theName(name),theImage(0), theAudioClip(0){    try { // 这try block是新加入的        if (imageFileName != "") {            theImage = new Image(imageFileName);        }        if (audioClipFileName != "") {            theAudioClip = new AudioClip(audioClipFileName);        }    }    catch (...) { // 捕获所有异常        delete theImage; // 完成必要的清除代码        delete theAudioClip;        throw; // 继续传递异常    }}

这似乎可行,除了构造函数和析构函数里面有少量的重复代码。

但是它没有考虑到下面这种情况。假设我们略微改动一下设计,让theImage 和theAudioClip是常量(constant)指针类型:

class BookEntry             // 通讯录中的条目{public:    BookEntry(const string& name, const string& imageFileName = "", const string& audioClipFileName = "");    ~BookEntry();private:    string theName;         // 人的姓名     Image * const theImage;     // 他们的图像    AudioClip * const theAudioClip; // 他们的一段声音片段};

一点思考:这里应该是指针常量,而不是常量指针,即指针指向的位置不能变,但它的内容能改变。
附上常量指针与指针常量的概念:
1.常量指针
定义:具有只能够读取内存中数据,却不能够修改内存中数据的属性的指针,称为指向常量的指针,简称常量指针。
声明:const int * p; int const * p;
2.指针常量
定义:指针常量是指指针所指向的位置不能改变,即指针本身是一个常量,但是指针所指向的内容可以改变。
声明:int * const p=&a;

必须通过BookEntry构造函数的成员初始化表来初始化这样的指针,因为再也没有其它地方可以给const指针赋值。通常会这样初始化theImage和theAudioClip:

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

这又回到了老问题,即抛出了异常,但是没有进行处理。

替代方法是定义私有类成员函数,来完成初始化的工作,并捕获异常。

class BookEntry             // 通讯录中的条目{public:    BookEntry(const string& name, const string& imageFileName = "", const string& audioClipFileName = "");    ~BookEntry();private:    string theName;         // 人的姓名     Image * const theImage;     // 他们的图像    AudioClip * const theAudioClip; // 他们的一段声音片段    Image * initImage(const string& imageFileName);    AudioClip * initAudioClip(const string& audioClipFileName);};BookEntry::BookEntry(const string& name,const string& imageFileName,const string& audioClipFileName)    : theName(name),theImage(initImage(imageFileName)),theAudioClip(initAudioClip(audioClipFileName)){}// theImage 被首先初始化,所以即使这个初始化失败也// 不用担心资源泄漏,这个函数不用进行异常处理。Image * BookEntry::initImage(const string& imageFileName){    if (imageFileName != "") return new Image(imageFileName);    else return 0;}// theAudioClip被第二个初始化, 所以如果在theAudioClip// 初始化过程中抛出异常,它必须确保theImage的资源被释放。// 因此这个函数使用try...catch AudioClip * BookEntry::initAudioClip(const string&audioClipFileName){    try     {        if (audioClipFileName != "")         {            return new AudioClip(audioClipFileName);        }        else return 0;    }    catch (...)     {        delete theImage;        throw;    }}

上面的程序的确不错,也解决了令我们头疼不已的问题。不过也有缺点,在原则上应该属于构造函数的代码却分散在几个函数里,这令我们很难维护。

所以最后、最优雅、最泪奔的结论是采用条款M9的建议,把theImage 和 theAudioClip指向的对象做为一个资源,被一些局部对象管理。这个解决方法建立在这样一个事实基础上:theImage 和theAudioClip是两个指针,指向动态分配的对象,因此当指针消失的时候,这些对象应该被删除。所以我们把theImage 和 theAudioClip raw指针类型改成对应的auto_ptr类型。

class BookEntry             // 通讯录中的条目{public:    BookEntry(const string& name, const string& imageFileName = "", const string& audioClipFileName = "");    ~BookEntry();private:    string theName;         // 人的姓名     const auto_ptr<Image> theImage; // 它们现在是auto_ptr对象    const auto_ptr<AudioClip> theAudioClip;};BookEntry::BookEntry(const string& name,const string& imageFileName,const string& audioClipFileName)    : theName(name),theImage(imageFileName != ""? new Image(imageFileName): 0),theAudioClip(audioClipFileName != ""? new AudioClip(audioClipFileName): 0){}BookEntry::~BookEntry() //--do nothing{}

如果在初始化theAudioClip时抛出异常,theImage已经是一个被完全构造的对象,所以它能被自动删除掉,就象theName一样。而且因为theImage 和 theAudioClip现在是包含在BookEntry中的对象,当BookEntry被删除时它们能被自动地删除。因此不需要手工删除它们所指向的对象。
综上所述,如果你用对应的auto_ptr对象替代指针成员变量,就可以防止构造函数在存在异常时发生资源泄漏,你也不用手工在析构函数中释放资源,并且你还能象以前使用非const指针一样使用const指针,给其赋值。

Item M11:禁止异常信息(exceptions)传递到析构函数外

Item M12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异

Item M13:通过引用(reference)捕获异常

Item M14:审慎使用异常规格(exception specifications)

Item M15:了解异常处理的系统开销

第七城市

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台