异常处理(一):利用析构函数避免泄漏资源

98 阅读1分钟

我正在参加「掘金·启航计划」

本文是《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;
}

上述实现完整实现代码

参考资料

  1. cppreference:unique_ptr
  2. 《More Effective C++:35个改善编程与设计的有效方法(中文版)》