问题的历史:
C++最初设计时强调性能,因此资源管理(内存、文件、流、套接字)更多是手动进行的。当异常发生时,需要清理所持有的资源。堆栈展开是一种机制,用于在抛出异常时正确结束函数的执行。
问题:
在抛出异常时,控制权立即转移到catch块,而中间函数"展开":它们的析构函数被调用,但显式调用释放函数可能会被跳过(例如,如果未使用自动释放资源的对象)。
解决方案:
在C++中,资源的释放应委托给析构函数,而不是手动调用释放函数。RAII(资源获取即初始化)模式是自动释放资源的明确方法。在堆栈展开期间,析构函数会被调用,因此资源会被释放,无论函数退出的路径如何。
代码示例:
#include <fstream> #include <stdexcept> void readFile(const std::string& filename) { std::ifstream file(filename); // 即使在异常情况下也会正确打开和关闭 if (!file.is_open()) { throw std::runtime_error("无法打开文件"); } // ... 读取文件 } // 即使在异常情况下,file也会关闭
关键特点:
如果代码在catch块中有delete ptr;,这是否足以清理内存?
不,如果在分配内存和catch块之间出现异常,内存可能不会被清理。最好使用std::unique_ptr或在析构函数中写delete。
代码示例:
void foo() { int* data = new int[10]; // ... throw std::runtime_error("失败"); delete[] data; // 在异常时不会被调用 }
堆栈展开能否跳过栈上对象的析构函数调用?
不能,所有局部对象(在异常抛出点之前未被销毁的)将按照创建的相反顺序被销毁,析构函数将被保证调用。
可以使用goto或longjmp退出try块并期望调用析构函数吗?
不。C++仅在因异常导致的堆栈展开时保证调用析构函数,而不在由于流控制不当(goto、setjmp/longjmp)时保证。
程序员使用new分配内存,处理异常,并在catch块中释放内存,但忘记了其他的函数退出路径。
优点:
缺点:
对所有资源使用std::unique_ptr和RAII类,释放不依赖于try/catch。
优点:
缺点: