我正在参加「掘金·启航计划」
本文是《More Effective C++:35个改善编程与设计的有效方法》对条款9:利用destructors避免泄漏资源的理解和补充完善。
条款9:利用destructors避免泄漏资源
假设你在为一个“小动物收养保护中心”编写一个软件,收养中心每天都会生成一个文件,由它安排当天的收养个案,书写一个程序来读取这些文件,为每个收养个案做适当的处理。
合理的想法是定义一个抽象基类ALA("Adorable Little Animal"),再派生出针对小狗和小猫的具体类。其中有个虚函数 processAdoption,负责“因动物种类而异”的必要处理动作。
class ALA {
public:
// 基类的析构函数为虚函数确保派生类析构会调用基类析构
virtual ~ALA() {
std::cout << "~ALA" << std::endl;
}
virtual void processAdoption() = 0;
};
class Puppy : public ALA {
public:
~Puppy() {
std::cout << "~Puppy" << std::endl;
}
void processAdoption() override {
std::cout << "Puppy::processAdoption" << std::endl;
// throw std::runtime_error("just for test");
}
};
假设通过new在堆上申请一个对象处理收养事项
{
ALA* puppy(new Puppy);
puppy->processAdoption();
delete puppy;
}
如果processAdoption抛出异常,那么便需要对异常处理
{
ALA* puppy(new Puppy);
try {
puppy->processAdoption();
} catch (std::exception& e) {
std::cerr << e.what() << '\n';
delete puppy; // 捕获异常调用delete来避免资源泄漏
}
delete puppy;
}
这样导致无论程序运行时正常还是异常都需要delete来释放资源,可以尝试将其其中于一处来做这件事情?因为不论函数如何结束,局部对象总是会在函数结束时被析构。于是,我们真正感兴趣的是,如何把 delete 动作从转移到函数内某个局部对象的destructor 内。解决办法就是,以一个“类似指针的对象”取代指针 ,而当这个类似指针的对象被(自动)销毁,令其 destructor 调用 delete。C++ 标准程序库提供了 std::unique_ptr(std::auto_ptr在C++11中已经被废除)。
上述实现可以采取std::unique_ptr来取代裸指针,并确保即使在异常抛出的情况下,堆上的对象依然可以被删除。
try {
std::cout << "1) 采用unique_ptr包装裸指针,避免资源泄漏,同时支持多态" << std::endl;
std::unique_ptr<ALA> puppy(new Puppy);
puppy->processAdoption();
} catch(const std::exception& e) {
std::cerr << e.what() << '\n';
}
隐藏在std::unique_ptr背后的观念——以一个对象存放“必须自动释放的资源”,并依赖该对象的destructor 释放。考虑图形界面(GUI)应用软件中的某个函数,它必须产生一个窗口以显示某些信息
// 可能会在抛出exception之后发生资源泄漏问题
void displayInfo(const Information& info) {
WINDOW_HANDLE w(createWindow()); // 产生窗口,取得资源
// 显示信息
destroyWindow(w); // 释放资源
}
如果在显示信息过程中产生异常,那么就可能会产生资源泄漏。解决方法和先前类似,设计一个类,令其在构造函数和析构函数内分别取得资源和释放资源。
class WINDOW_HANDLE {
public:
void displayInfo() {
std::cout << "Display Info" << std::endl;
// throw std::runtime_error("displayInfo exception just for test");
}
};
WINDOW_HANDLE createWindow() {
WINDOW_HANDLE wh;
std::cout << "createWindow" << std::endl;
return wh;
}
void destroyWindow(const WINDOW_HANDLE& handle) {
std::cout << "destroyWindow" << std::endl;
}
class WindowHandle {
public:
explicit WindowHandle(WINDOW_HANDLE handle) : w(handle) {}
~WindowHandle() { destroyWindow(w); }
// 隐式转换,将WindowHandle转换为WINDOW_HANDLE
operator WINDOW_HANDLE() const { return w; }
private:
WINDOW_HANDLE w;
WindowHandle(const WindowHandle&);
WindowHandle& operator=(const WindowHandle&);
};
假设在displayInfo处理中抛出异常,也可以确保调用destroyWindow来释放资源。
try {
std::cout << "\n2) 将WINDOW_HANDLE封装在WindowHandle对象内来避免资源泄漏" << std::endl;
WindowHandle wh2(createWindow());
static_cast<WINDOW_HANDLE>(wh2).displayInfo();
} catch(const std::exception& e) {
std::cerr << e.what() << '\n';
}
return 0;
}
在WindowHandle提供了operator WINDOW_HANDLE() const隐式转换来实现可以在以WINDOW_HANDLE入参的函数中支持以WindowHandle来入参,例如
// 展示隐式转换带来的方便之处
// 由于WindowHandle支持隐式转换为WINDOW_HANDLE, 函数入参可以为WindowHandle
void ConvenientWindowHandlerDisplay(WINDOW_HANDLE winHandle) {
winHandle.displayInfo();
}
WindowHandle wh2(createWindow());
ConvenientWindowHandlerDisplay(wh2);
unique_ptr支持自定义deleter
void CloseFile(std::FILE* fd) {
std::cout << "close file" << std::endl;
std::fclose(fd);
}
int main(int argc, char* argv[]) {
try {
{
std::cout << "\n3) Custom deleter demo" << std::endl;
std::ofstream("demo.txt") << 'x'; // prepare the file to read
using UniqueFilePtrType = std::unique_ptr<std::FILE, decltype(&CloseFile)>;
UniqueFilePtrType ufd(std::fopen("demo.txt", "r"), &CloseFile);
if (ufd) {
std::cout << char(std::fgetc(ufd.get())) << std::endl;;
}
}
{
std::cout << "\n4) Custom lambda-expression deleter and exception safety demo"
<< std::endl;
std::unique_ptr<Puppy, void(*)(Puppy*)> p(new Puppy, [](Puppy* ptr) {
std::cout << "destroying from a custom deleter...\n";
delete ptr;
});
// throw std::runtime_error("custom lambda-expression deleter : just for test");
}
} catch(const std::exception& e) {
std::cerr << e.what() << '\n';
}
return 0;
}
上述实现完整实现代码
参考资料
- cppreference:unique_ptr
- 《More Effective C++:35个改善编程与设计的有效方法(中文版)》