C++ 错误处理:从 try/catch 到 RAII 和 noexcept
程序的可靠性直接取决于它对意外情况的反应方式。在 C++ 中,异常是实现这一目标的主要机制。在本文中,我们将分析经典的 try/catch/throw 结构,并探讨替代和现代方法:RAII、noexcept 以及无异常的错误处理。
1. 基本机制:try, catch, throw
异常是一个在发生错误时被“抛出”(throw)的对象。程序可以捕获它(catch)并执行纠正操作,而无需完全中断工作。
语法
#include <iostream>#include <stdexcept>
double divide(double a, double b) { if (b == 0.0) { throw std::runtime_error("除以零!"); } return a / b;}
int main() { try { double result = divide(10.0, 0.0); std::cout << "结果: " << result << std::endl; } catch (const std::runtime_error& e) { std::cerr << "错误: " << e.what() << std::endl; } return 0;}工作规则
- throw — 创建对象的副本并将控制权传递给处理程序。
- try — 可能发生异常的代码块。
- catch — 按类型捕获异常。可以指定多个
catch块。 - 如果异常未被捕获,则调用
std::terminate()。
按基类捕获
所有标准异常都继承自 std::exception。因此,可以编写一个通用处理程序:
try { // 代码} catch (const std::exception& e) { std::cerr << "标准错误: " << e.what() << std::endl;} catch (...) { std::cerr << "未知异常" << std::endl;}catch (...) 块捕获所有异常,但无法访问异常对象。
2. 构造函数和析构函数中的异常
构造函数
如果构造函数抛出异常,则认为对象未创建,并且不会调用其析构函数。这对于资源管理很重要。示例:
class Resource {public: Resource() { std::cout << "资源已分配" << std::endl; throw std::runtime_error("初始化错误"); } ~Resource() { std::cout << "资源已释放" << std::endl; }};
int main() { try { Resource r; } catch (...) { std::cout << "异常已捕获" << std::endl; } // Resource 的析构函数将不会被调用! return 0;}析构函数
从析构函数抛出异常是不好的做法。如果在栈展开期间调用析构函数,并且它抛出一个新异常,程序将异常终止。始终将析构函数编写为 noexcept(默认情况下它们就是如此)。
3. RAII — 自动资源管理
RAII(资源获取即初始化)惯用法是安全代码的关键。资源(内存、文件、互斥锁)在构造函数中获取,并在析构函数中释放。如果发生异常,所有已创建对象的析构函数会自动调用(栈展开),并且资源被释放。
使用智能指针 std::unique_ptr 的示例:
#include <memory>#include <iostream>
void process() { auto ptr = std::make_unique<int>(42); // 如果这里发生异常, // unique_ptr 将自动释放内存 throw std::runtime_error("proc 中的错误");}