编程C++ 后端开发人员

描述 C++ 中异常处理时 stack unwinding 和对象销毁之间的区别。如何确保在发生异常时正确释放资源?

用 Hintsage AI 助手通过面试

答案。

在 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 会自动被调用 }

关键特性:

  • stack unwinding 自动调用栈中所有对象的析构函数。
  • RAII 确保无论是否发生异常,资源都能被正确释放。
  • 在 catch 块中手动释放资源的频率很低:析构函数会更可靠地做到这一点。

有陷阱的问题。

为什么不能仅仅使用 try-catch 并在 catch 块中手动调用 delete 或 fclose 来避免泄漏?

回答:

这既不方便也不可靠:很容易忘记关闭资源,尤其是在有多个退出点或嵌套资源的情况下。即使异常"穿越"函数,析构函数也会被调用,catch 不是必须的。

如果堆(heap)中的对象没有在 stack unwinding 中"封装"在栈上的对象中,它们会被销毁吗?

回答:

不会。stack unwinding 只对在栈中分配的对象调用析构函数。为了正确销毁堆对象,它们必须由栈中的对象拥有(例如,通过智能指针)。

如果在 stack unwinding 过程中,析构函数本身抛出了异常,会发生什么?

回答:

如果在 stack unwinding 过程中,销毁某个对象时抛出了第二个异常,程序将通过调用 std::terminate() 终止。绝不要在析构函数中抛出异常!

常见错误和反模式

  • 不在析构函数中释放资源,而仅依赖于在 catch 中的手动管理。
  • 从析构函数中抛出异常。
  • 不使用智能指针来管理堆资源。
  • 忽视嵌套资源(例如,在结构体内部的文件)。

生活中的例子

负面案例

开发人员在 catch 块中手动关闭文件和释放内存,而没有使用 RAII。

优点:

  • 显式管理资源。

缺点:

  • 如果在 throw 前退出到 catch,将无法避免泄漏。
  • 维护和修改代码很复杂,容易忘记释放。

正面案例

文件和资源被封装在 RAII 包装类中。结果:析构函数中释放资源,无论是 throw/catch。

优点:

  • 资源释放的可靠性。
  • 更少的代码,更容易维护。

缺点:

  • 需要能够编写 RAII 包装。
  • 添加新的资源类型需要新的类。