More Effective C++条款10:在constructors内阻止资源泄露

想像你正在开发一个多媒体通信薄软件,这个软件可以放置包括人名, 地址,电话号码等文字,以及一张个人相片和一段个人声音。为了简单起见,本文中假设只包含个人相片和声音。代码实现下:

#include <stdio.h>
#include <iostream>
using namespace std;

class Image
{
public:
    Image(const string& imageDataFileName) {  printf("Image constructor\r\n");}
    ~Image() { printf("Image destructor\r\n"); }
};

class AudioClip
{
public:
    AudioClip(const string& audioDataFileName) 
    { 
        printf("AudioClip constructor\r\n"); 
    }
    ~AudioClip() { printf("AudioClip destructor\r\n"); }
};


class BookEntry
{
public:
    BookEntry(string &imageDataFileName, string& audioDataFileName);   
    ~BookEntry();
private:
    Image *theImage;
    AudioClip *theAudioClip;
};

BookEntry::BookEntry(string &imageDataFileName, string& audioDataFileName):theImage(0),theAudioClip(0)  
{
    printf("BookEntry constructor\r\n");

    if(imageDataFileName != "")
    {
        theImage = new Image(imageDataFileName);
    }

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

BookEntry::~BookEntry()
{
    printf("BookEntry destructor\r\n");
    delete theImage;
    delete theAudioClip;
}

int main()
{
    string imageDataFileName("abc");
    string audioDataFileName("def");
    BookEntry bookEntry(imageDataFileName, audioDataFileName); 
    return 0;
}

一切看起来好像没有什么问题。确实如此!在没有抛异常时,没有任何问题!但是假如现在BookEntry的构造函数在new AudioClip对象时抛异常了会怎么样呢?(完全有可能,比如空间不够了new操作符就会抛出异常), 已经new好的Image对象会被释放吗? 看下面代码:

#include <stdio.h>
#include <iostream>
using namespace std;

class Image
{
public:
    Image(const string& imageDataFileName) {  printf("Image constructor\r\n");}
    ~Image() { printf("Image destructor\r\n"); }
};

class AudioClip
{
public:
    AudioClip(const string& audioDataFileName) 
    { 
        printf("AudioClip constructor\r\n"); 
        throw invalid_argument("throw execption for testing!"); 

    }
    ~AudioClip() { printf("AudioClip destructor\r\n"); }
};


class BookEntry
{
public:
    BookEntry(string &imageDataFileName, string& audioDataFileName);   
    ~BookEntry();
private:
    Image *theImage;
    AudioClip *theAudioClip;
};

BookEntry::BookEntry(string &imageDataFileName, string& audioDataFileName):theImage(0),theAudioClip(0)  
{
    printf("BookEntry constructor\r\n");

    if(imageDataFileName != "")
    {
        theImage = new Image(imageDataFileName);
    }

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

BookEntry::~BookEntry()
{
    printf("BookEntry destructor\r\n");
    delete theImage;
    delete theAudioClip;
}

int main()
{
    string imageDataFileName("abc");
    string audioDataFileName("def");
    try
    {
        BookEntry bookEntry(imageDataFileName, audioDataFileName);
    }
    catch (invalid_argument e)
    {
        printf("%s\r\n", e.what());
        return -1;
    }

    return 0;
}

运行结果如下:

BookEntry constructor
Image constructor
AudioClip constructor
throw execption for testing!

BookEntry的析构函数没有被调用,Image对象并没有被释放,内存泄露了!这是因为面对尚未完全构造好的对象,C++拒绝调用其destructor。 如果destructor被调用于一个尚未完全构造好的对象身上, 这个destructor如何知道该做些什么事情呢?它唯一能够知道的机会就是:被加到对象的那些数据身上附带有某种指示,指示constructor进行到什么程度。那么destructor就可以检查这些数据(或许能够)理解应该如何应对。如此繁重的笔记工作会减低constructor的速度,使每一个对象变得更庞大。C++没有这样做。所以C++不会自动清理那些“构造期间抛出exception”的对象,所以你必须设计你的constructors,使他们在那种情况下亦能自我清理。

通常只需要将所有可能的exceptions捕捉起来,执行某种清理工作,然后重新抛出异常,使它继续传播出去即可。如下面代码:

#include <stdio.h>
#include <iostream>
using namespace std;

class Image
{
public:
    Image(const string& imageDataFileName) {  printf("Image constructor\r\n");}
    ~Image() { printf("Image destructor\r\n"); }
};

class AudioClip
{
public:
    AudioClip(const string& audioDataFileName) 
    { 
        printf("AudioClip constructor\r\n"); 
        throw invalid_argument("throw execption for testing!");

    }
    ~AudioClip() { printf("AudioClip destructor\r\n"); }
};


class BookEntry
{
public:
    BookEntry(string &imageDataFileName, string& audioDataFileName);   
    ~BookEntry();
private:
    Image *theImage;
    AudioClip *theAudioClip;
    void cleanup();
};

BookEntry::BookEntry(string &imageDataFileName, string& audioDataFileName):theImage(0),theAudioClip(0)  
{
    printf("BookEntry constructor\r\n");

    try
    {
        if(imageDataFileName != "")
        {
            theImage = new Image(imageDataFileName);
        }

        if(audioDataFileName != "")
        {
            theAudioClip = new AudioClip(audioDataFileName);
        }
    }
    catch(invalid_argument e)
    {
        printf("catched a exception in BookEntry constructor, now release the resource!\r\n");
        cleanup();
        throw;
    }
}

BookEntry::~BookEntry()
{
    printf("BookEntry destructor\r\n");
    cleanup();
}

void BookEntry::cleanup()
{
    printf("BookEntry cleanup\r\n");
    delete theImage;
    delete theAudioClip;
}

int main()
{
    string imageDataFileName("abc");
    string audioDataFileName("def");
    try
    {
        BookEntry bookEntry(imageDataFileName, audioDataFileName);
    }
    catch (invalid_argument e)
    {
        printf("%s\r\n", e.what());
        return -1;
    }

    return 0;
}

运行结果:

BookEntry constructor
Image constructor
AudioClip constructor
catched a exception in BookEntry constructor, now release the resource!
BookEntry cleanup
Image destructor
throw execption for testing!

进一步思考,如果theImage和theAudioClip都变成常量了会怎么样:

    Image * const theImage;
    AudioClip * const theAudioClip;

常量指针必须通过BookEntry constructor的成员初始链表加以初始化,如下所示:

BookEntry::BookEntry(string &imageDataFileName, string& audioDataFileName):
theImage(imageDataFileName != "" ? new Image(imageDataFileName):0),
theAudioClip(audioDataFileName != "" ? new AudioClip(audioDataFileName):0) 
{
    printf("BookEntry constructor\r\n");
}

但这却导致了我们最初想消除的问题:如果在theAudioClip初始化期间发生了exception, theImage所指对象不能销毁。此外我们也无法借此在constructor内加上try/catch语句来解决此问题,因为try和catch都是语句,而初始化链表只接受表达式。尽管如此,我们还有一种办法,就是封装对象的构造过程在其它private function内,让theImage和theAudioClip在其中获得初值,如下所示:

class BookEntry
{
public:
    BookEntry(string& imageDataFileName, string& audioDataFileName);   
    ~BookEntry();
private: 
    Image * InitImage(string& imageDataFileName);
    AudioClip * InitAudioClip(string& audioDataFileName);
    Image * const theImage;
    AudioClip * const theAudioClip;
};

BookEntry::BookEntry(string& imageDataFileName, string& audioDataFileName):
theImage(InitImage(imageDataFileName)),
theAudioClip(InitAudioClip(audioDataFileName)) 
{
    printf("BookEntry constructor\r\n");
}

对于第一个private函数InitImage, 因为image是最开始初始化的,不用捕捉异常。对于第二个private函数,需要捕捉异常并且在异常时销毁theImage。如下所示:

Image * BookEntry::InitImage(string& imageDataFileName)
{
    if(imageDataFileName != "") return new Image(imageDataFileName);

    return 0;
}

AudioClip * BookEntry::InitAudioClip(string& audioDataFileName)
{
    try 
    {
        if(audioDataFileName != "") return new AudioClip(audioDataFileName);
    }
    catch(invalid_argument e)
    {
        printf("catched a exception in %s, now release the resources\r\n",__func__);
        delete theImage;
        throw;
    }
    
    return 0;
}

这样做能解决问题,但是这样有没有不好的地方呢?有的!初始化函数要写成多个,并且初始化函数之间是有耦合性的,第二个调用的函数要释放第一个产生的对象,第三个调用的要释放第二个以及第一个产生的对象...代码不够clean.  那么有没有更加好一点的方案呢?答案是智能指针!将theImage和theAudioClip所指对象为资源,交给局部对象来管理。不论theImage和theAudioClip作为局部变量何时释放(生命周期到),其所指资源一定得到释放。在发生异常时,BookEntry对象被销毁(虽然还没完全构造好), 同时其下所有的对象(theImage,theAudioClip)也同样会被销毁,那么根据智能指针的特性,当指针销毁时,其所指资源一定会得到释放。完整代码如下:

#include <stdio.h>
#include <iostream>
#include <memory>
using namespace std;

class Image
{
public:
    Image(const string& imageDataFileName) {  printf("Image constructor\r\n");}
    ~Image() { printf("Image destructor\r\n"); }
};

class AudioClip
{
public:
    AudioClip(const string& audioDataFileName) 
    { 
        printf("AudioClip constructor\r\n"); 
        throw invalid_argument("throw execption for testing!");

    }
    ~AudioClip() { printf("AudioClip destructor\r\n"); }
};


class BookEntry
{
public:
    BookEntry(string &imageDataFileName, string& audioDataFileName);   
    ~BookEntry();
private:
    const unique_ptr<Image> theImage;
    const unique_ptr<AudioClip> theAudioClip;
};

BookEntry::BookEntry(string &imageDataFileName, string& audioDataFileName):
theImage(imageDataFileName != "" ? new Image(imageDataFileName):0),
theAudioClip(audioDataFileName != "" ? new AudioClip(audioDataFileName):0) 
{
    printf("BookEntry constructor\r\n");
}

BookEntry::~BookEntry()
{
    printf("BookEntry destructor\r\n");
}

int main()
{
    string imageDataFileName("abc");
    string audioDataFileName("def");
    try
    {
        BookEntry bookEntry(imageDataFileName, audioDataFileName);
    }
    catch (invalid_argument e)
    {
        printf("%s\r\n", e.what());
        return -1;
    }

    return 0;
}

运行结果:

Image constructor
AudioClip constructor
Image destructor
throw execption for testing!

至此,问题完美解决!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一条叫做nemo的鱼

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值