在 C++ 中,异常处理伴随着 stack unwinding 机制——在异常发生之前自动销毁(析构)栈中创建的局部对象。这一现象对于正确释放资源至关重要。
最初,在设计 C++ 时,缺少内置的垃圾收集机制,资源(内存、文件、套接字)的释放任务落在了程序员身上。异常处理应该确保在代码因为错误而回滚时能够正确释放资源。
如果没有在异常发生时自动销毁对象的机制,就会导致资源泄漏。如果在每个 catch 块中手动释放资源,那么代码会变得复杂且不可靠。
C++ 实现了 stack unwinding,当抛出异常时,所有在 throw 之前在栈中可见的对象会按创建的相反顺序被销毁。它们的析构函数会被自动调用。
经典的持续释放资源的方式是使用 RAII(资源获取即初始化)模式:所有资源都被封装在对象中,析构时释放其拥有的资源。
代码示例:
#include <iostream> #include <stdexcept> struct FileHandle { FILE* file; FileHandle(const char* path) { file = fopen(path, "r"); if (!file) throw std::runtime_error("无法打开文件"); } ~FileHandle() { if (file) fclose(file); } }; void processFile(const char* path) { FileHandle fh(path); // 如果发生异常,将会回滚 // ... 操作文件 throw std::runtime_error("某些错误"); // fclose 会自动被调用 }
关键特性:
为什么不能仅仅使用 try-catch 并在 catch 块中手动调用 delete 或 fclose 来避免泄漏?
回答:
这既不方便也不可靠:很容易忘记关闭资源,尤其是在有多个退出点或嵌套资源的情况下。即使异常"穿越"函数,析构函数也会被调用,catch 不是必须的。
如果堆(heap)中的对象没有在 stack unwinding 中"封装"在栈上的对象中,它们会被销毁吗?
回答:
不会。stack unwinding 只对在栈中分配的对象调用析构函数。为了正确销毁堆对象,它们必须由栈中的对象拥有(例如,通过智能指针)。
如果在 stack unwinding 过程中,析构函数本身抛出了异常,会发生什么?
回答:
如果在 stack unwinding 过程中,销毁某个对象时抛出了第二个异常,程序将通过调用 std::terminate() 终止。绝不要在析构函数中抛出异常!
开发人员在 catch 块中手动关闭文件和释放内存,而没有使用 RAII。
优点:
缺点:
文件和资源被封装在 RAII 包装类中。结果:析构函数中释放资源,无论是 throw/catch。
优点:
缺点: