想像你正在开发一个多媒体通信薄软件,这个软件可以放置包括人名, 地址,电话号码等文字,以及一张个人相片和一段个人声音。为了简单起见,本文中假设只包含个人相片和声音。代码实现下:
#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!
至此,问题完美解决!