问题实例
假设我们正在开发一个多媒体电话簿,它不仅可以存储姓名地址号码外,还能存储照片和声音。该类具体形式如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class 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 string& audioClipFileName = "");
~BookEntry();
void addPhoneNumber(const PhoneNumber& number);
...
private:
string theName;
string theAddress;
list<PhoneNumber> thePhones;
Image *theImage;
AudioClip *theAudioClip;
};
因为条目必须存在姓名,所以不存在默认构造函数(More Effective C++ 3)。
对于条目的构造和析构函数,有具体实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14BookEntry::BookEntry(const string& name,const string& address,
const string& imageFileName,Const string& audioClipFileName)
:theName(name), theAddress(address),theImage(nullptr), theAudioClip(nullptr){
if (imageFileName != "") {
theImage = new Image(imageFileName);
}
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}
}
BookEntry::~BookEntry(){
delete theImage;
delete theAudioClip;
}
这一切看起来运作良好,但如果函数中出现了异常…
问题剖析
如果BookEntry的构造函数正在执行中,突然发生异常:1
2
3if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}
异常的抛出原因,可能是operator new不能够分配足够内存,也可能是audio的构造函数自己抛出了异常。不管如何,这个异常将传递到建立BookEntry对象的地方。
假设建立audio时抛出了异常(且控制权此时在Entry构造函数外),那么已经构建好的image对象该由谁来删除呢?无疑该资源应该由Entry的析构函数释放,但在实际情况中,Entry析构函数并未被调用,原因很简单:C++只能对完全构造的对象进行析构,只有一个对象的构造函数完全运行完毕,这个对象才被定义为完全构造。
也许有人会想到使用try—catch主动处理异常:1
2
3
4
5
6
7
8
9
10
11
12
13void testBookEntryClass(){
BookEntry *pb = nullptr;
try {
pb = new BookEntry("Addison-Wesley Publishing Company",
"One Jacob Way, Reading, MA 01867");
...
}
catch (...) {
delete pb;
throw;
}
delete pb;
}
这并没有什么用,因为new操作并没有完成,pb依旧是一个nullptr,delete只不过删了一个空指针而已。
c++禁止为尚未完成构造的对象进行析构,因为允许该操作需要记录构造函数执行了多少步,这会造成额外的开销。
解决方案
为了解决资源泄漏这一问题,处理方法是在构造函数中捕获所有异常,捕获后执行清除操作,最后再重新抛出异常让它继续传递:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17BookEntry::BookEntry(const string& name,const string& address,
const string& imageFileName,Const string& audioClipFileName)
:theName(name), theAddress(address),theImage(nullptr), theAudioClip(nullptr){
try {
if (imageFileName != "") {
theImage = new Image(imageFileName);
}
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}
}
catch (...) {
delete theImage;
delete theAudioClip;
throw;
}
}
非指针成员在构造函数被调用之前就完成了初始化,属于完全构造,因此会被自动释放。
显然,catch语句块中的清除操作几乎与析构函数一摸一样,为了避免代码重复,我们定义一个私有的helper function,让构造函数与析构函数都调用它,如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class BookEntry {
public:
... // 同上
private:
...
void cleanup();
};
void BookEntry::cleanup(){
delete theImage;
delete theAudioClip;
}
BookEntry::BookEntry(const string& name,const string& address,
const string& imageFileName,Const string& audioClipFileName)
:theName(name), theAddress(address),theImage(nullptr), theAudioClip(nullptr){
try {
...
}
catch (...) {
cleanup();
throw;
}
}
BookEntry::~BookEntry(){
cleanup();
}
异常产生于构造函数调用之前
假设image与audio是一个const指针,因此它们必须通过初始化列表来完成初始化(不允许赋值):1
2
3
4
5
6BookEntry::BookEntry(const string& name,const string& address,
const string& imageFileName,Const string& audioClipFileName)
:theName(name), theAddress(address),
theImage(imageFileName != ""? new Image(imageFileName): nullptr),
theAudioClip(audioClipFileName != ""? new AudioClip(audioClipFileName): nullptr)
{}
倘若常量指针初始化时抛出了异常….这一次情况发生在构造函数被调用之前,所以无法在构造函数中使用try catch语句。
解决方案
新建初始化函数
我们可以在私有成员函数中,用一些函数返回指针指向初始化过的image与audio:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30class BookEntry {
public:
...
private:
...
Image * initImage(const string& 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首先初始化,初始化失败也不会有内存泄漏,无需考虑异常
Image * BookEntry::initImage(const string& imageFileName){
if (imageFileName != "") return new Image(imageFileName);
else return nullptr;
}
//初始化失败必须要保证Image资源被释放
AudioClip * BookEntry::initAudioClip(const string&audioClipFileName){
try {
if (audioClipFileName != "")
return new AudioClip(audioClipFileName);
else return 0;
}
catch (...) {
delete theImage;
throw;
}
}
该方法的缺点在于原本属于构造函数的代码却分散在各个函数中,维护极为不易,并且对象增加了新成员后代码也需要重新编写。
RAII
我们可以运用RAII思想,把audio与image指向的对象作为资源托管给smart-ptr:1
2
3
4
5
6
7
8
9
10
11
12
13
14class BookEntry {
public:
...
private:
...
const auto_ptr<Image> theImage;
const auto_ptr<AudioClip> theAudioClip;
};
BookEntry::BookEntry(const string& name,const string& address,
const string& imageFileName,const string& audioClipFileName)
:theName(name), theAddress(address),
theImage(imageFileName != ""? new Image(imageFileName): nullptr),
theAudioClip(audioClipFileName != ""? new AudioClip(audioClipFileName): nullptr)
{}
如此一来,audio构造出现异常时,image已经是一个完成构造的完全体,可以被直接清除,同时所有资源均由对象托管,析构函数无需执行任何工作。