《More Effective C++:35个改善编程与设计的有效方法》
读书笔记:在constructors内阻止资源泄漏(resource leak)
C++ 构造函数中阻止资源泄漏的实践探索
在 C++ 开发中,资源管理始终是关键议题,尤其是在构造函数(constructors)执行期间,若遭遇异常(exception),极易引发资源泄漏(resource leak)问题。本文将结合多媒体通信簿软件的开发场景,深入剖析构造函数内资源泄漏的成因,并探寻行之有效的解决策略。
一、场景与初始设计
(一)需求背景
我们要开发一款多媒体通信簿软件,需存储联系人的姓名、地址、电话号码等文字信息,以及个人相片(Image 类管理 )和声音片段(AudioClip 类管理 )。核心类 BookEntry
用于封装这些信息,其初步设计如下:
class 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;
};
(二)构造函数与析构函数初版实现
构造函数尝试依据传入的文件名初始化 theImage
和 theAudioClip
,析构函数负责释放这些指针指向的资源:
BookEntry::BookEntry(const string& name, const string& address,
const string& imageFileName, const string& audioClipFileName)
: theName(name), theAddress(address), theImage(0), theAudioClip(0) {
if (imageFileName != "") {
theImage = new Image(imageFileName);
}
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}
}
BookEntry::~BookEntry() {
delete theImage;
delete theAudioClip;
}
此设计在正常流程下能正常工作,可一旦构造函数执行中抛出异常(比如 new Image
或 new AudioClip
失败),问题就会显现。
二、异常引发的资源泄漏问题
(一)构造未完成对象的析构困境
C++ 规定,只有构造完成的对象才会调用析构函数。若 BookEntry
构造函数执行到 new AudioClip
时抛出异常,此时 theImage
可能已成功分配资源,但由于对象未完全构造,BookEntry
的析构函数不会被调用,theImage
指向的资源就会泄漏。
void testBookEntryClass() {
try {
BookEntry b("Test", "Address", "image.jpg", "audio.wav");
} catch (...) {
// 若构造 b 时抛出异常,b 未完全构造,析构函数不执行
// theImage 若已分配,资源泄漏
}
}
(二)指针成员为 const 时的初始化难题
若 theImage
和 theAudioClip
是 const
指针,就必须在成员初始化列表初始化。如下错误示例:
class BookEntry {
private:
Image* const theImage;
AudioClip* const theAudioClip;
};
BookEntry::BookEntry(/* 参数 */)
: theName(name), theAddress(address),
theImage(imageFileName != "" ? new Image(imageFileName) : 0),
theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName) : 0) {
// 若 theAudioClip 初始化抛异常,theImage 已分配但无法在构造函数内清理
}
成员初始化列表里无法使用 try-catch
,若 theAudioClip
初始化异常,theImage
资源会泄漏。
三、解决方案探索
(一)构造函数内使用 try-catch
在构造函数函数体里用 try-catch
,捕获异常时清理已分配资源:
BookEntry::BookEntry(/* 参数 */)
: theName(name), theAddress(address), theImage(0), theAudioClip(0) {
try {
if (imageFileName != "") {
theImage = new Image(imageFileName);
}
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}
} catch (...) {
delete theImage;
delete theAudioClip;
throw;
}
}
这样,构造函数内分配资源抛异常时,能及时清理已分配资源,再重新抛出异常让上层处理。不过,若类有多个资源需分配,重复写 delete
代码会冗余,可提取到私有清理函数:
class BookEntry {
private:
void cleanup() {
delete theImage;
delete theAudioClip;
}
};
BookEntry::BookEntry(/* 参数 */) {
try {
// 资源分配逻辑
} catch (...) {
cleanup();
throw;
}
}
BookEntry::~BookEntry() {
cleanup();
}
(二)利用智能指针(auto_ptr 或现代智能指针)
C++ 中的 auto_ptr
(C++11 后推荐用 unique_ptr
等更安全智能指针 )可自动管理资源。将指针成员替换为智能指针:
#include <memory>
class BookEntry {
private:
auto_ptr<Image> theImage;
auto_ptr<AudioClip> theAudioClip;
};
BookEntry::BookEntry(/* 参数 */)
: theName(name), theAddress(address),
theImage(imageFileName != "" ? new Image(imageFileName) : 0),
theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName) : 0) {
// 无需手动清理,智能指针析构时自动释放资源
}
BookEntry::~BookEntry() {
// 智能指针自动管理,无需手动 delete
}
若构造函数中 theAudioClip
初始化抛异常,theImage
作为已构造的智能指针对象,其析构函数会自动调用,释放 Image
资源,避免泄漏。现代 C++ 建议用 unique_ptr
或 shared_ptr
,用法类似且更安全:
class BookEntry {
private:
unique_ptr<Image> theImage;
unique_ptr<AudioClip> theAudioClip;
};
(三)提取资源初始化到辅助函数
把资源初始化逻辑放到私有辅助函数,辅助函数内处理异常:
class BookEntry {
private:
Image* initImage(const string& imageFileName) {
if (imageFileName != "") {
try {
return new Image(imageFileName);
} catch (...) {
// 可记录日志等,再重新抛出或处理
throw;
}
}
return 0;
}
AudioClip* initAudioClip(const string& audioClipFileName) {
if (audioClipFileName != "") {
try {
return new AudioClip(audioClipFileName);
} catch (...) {
// 若 audioClip 初始化失败,需清理已分配的 image 资源
delete theImage;
throw;
}
}
return 0;
}
};
BookEntry::BookEntry(/* 参数 */)
: theName(name), theAddress(address),
theImage(initImage(imageFileName)),
theAudioClip(initAudioClip(audioClipFileName)) {
// 构造函数体
}
此方式虽能处理部分异常,但逻辑复杂,尤其是一个资源初始化失败需清理另一个已分配资源时,耦合度高,维护难。
四、最佳实践与总结
- 优先使用智能指针:如
unique_ptr
、shared_ptr
,它们能自动管理资源,构造函数异常时,已构造的智能指针对象析构会释放资源,简化代码且减少泄漏风险。 - 构造函数内合理使用 try-catch:若需在构造函数处理异常(如记录日志、清理部分资源),用
try-catch
捕获并清理,再重新抛出异常让上层处理。 - 避免复杂资源初始化耦合:将资源初始化逻辑拆分,降低不同资源初始化的依赖,一个资源初始化失败,不影响其他已成功初始化资源的清理。
总之,C++ 构造函数内阻止资源泄漏,要结合智能指针、异常处理和合理代码结构设计。现代 C++ 特性(如智能指针)能大幅简化资源管理,提升代码健壮性,减少手动管理资源的错误。开发时,应充分利用这些工具,保障程序在异常场景下也能正确释放资源,避免泄漏。