28、异常处理:异常机制(try/throw/catch)、标准异常类、noexcept说明符、异常安全保证
异常处理,说白了就是给程序装个「安全气囊」。
我刚开始写C++那会儿,总觉得异常处理是可有可无的东西。直到有一次,线上服务因为一个空指针直接崩溃,数据丢了整整两小时……嗯,从那以后,我再也不敢轻视异常了。
一、异常机制:try/throw/catch
C++的异常机制其实就三个关键字:try、throw、catch。它们配合起来,就像一套「报修-上报-处理」的流程。
- try:圈定可能出问题的代码区域
- 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::exception& e) {
std::cerr << "捕获异常: " << e.what() << std::endl;
}
return 0;
}
你想想看,如果没有异常机制,除零错误要么让程序崩溃,要么你得在每个调用处写一堆if判断。异常机制把错误处理从正常逻辑中剥离出来,代码干净多了。
我的习惯:我一般按引用捕获异常(
catch (const std::exception& e)),避免对象切片,也省一次拷贝。
二、标准异常类
C++标准库提供了一套异常类体系,根类是std::exception。我个人觉得,用好这套体系,比你自己从头定义异常类要靠谱得多。
| 异常类 | 说明 | 常见场景 |
|---|---|---|
std::exception |
所有标准异常的基类 | 捕获所有标准异常 |
std::logic_error |
逻辑错误,可避免 | 参数校验失败 |
std::invalid_argument |
无效参数 | 函数参数不合法 |
std::out_of_range |
越界访问 | vector::at() 越界 |
std::runtime_error |
运行时错误,难避免 | 文件打开失败、网络超时 |
std::bad_alloc |
内存分配失败 | new 分配失败 |
我曾经踩过的坑:自定义异常类时,忘了继承
std::exception,结果 catch 基类时死活抓不到。后来我养成了习惯:所有自定义异常都继承 std::runtime_error,省心。
三、noexcept 说明符
noexcept 是C++11引入的关键字,用来告诉编译器:这个函数不会抛出异常。
void safe_func() noexcept {
// 这里如果抛出异常,程序会直接终止
std::cout << "这个函数保证不抛异常" << std::endl;
}
void maybe_throw() noexcept(false) {
// 明确表示可能抛异常
throw std::runtime_error("可能会抛");
}
为什么要有 noexcept?说白了,编译器知道函数不抛异常后,可以做更多优化。比如移动构造函数,如果标记为 noexcept,标准库容器在重新分配内存时会优先使用移动而非拷贝,性能提升明显。
避坑指南:我曾经把一个析构函数标记为
noexcept,结果里面调用了可能抛异常的函数。程序在析构时异常逃逸,直接调用了 std::terminate()。记住:析构函数默认就是 noexcept,千万别在里面抛异常。
四、异常安全保证
异常安全保证,说白了就是「出错了怎么办」。C++社区把它分成三个等级:
- 基本保证:出异常后,程序状态仍然有效,但可能不是原来的值
- 强保证:出异常后,程序状态回滚到操作之前
- 不抛出保证:绝对不会抛异常
class Account {
int balance_;
public:
// 强保证:要么成功,要么状态不变
void deposit(int amount) {
if (amount < 0) throw std::invalid_argument("金额不能为负");
int new_balance = balance_ + amount; // 先计算
if (new_balance < balance_) throw std::overflow_error("溢出");
balance_ = new_balance; // 最后才修改状态
}
// 不抛出保证
int get_balance() const noexcept {
return balance_;
}
};
我建议你写代码时,至少保证「基本保证」。如果条件允许,尽量做到「强保证」。至于「不抛出保证」,留给那些绝对不能失败的操作,比如析构函数、swap函数。
我的经验:实现强保证的常用手法是「copy-and-swap」。先拷贝一份,在拷贝上操作,如果成功就swap,如果失败就丢弃拷贝。这样原始状态永远不会被破坏。
五、异常处理的核心流程图
六、总结
异常处理不是锦上添花,而是工程质量的底线。我见过太多项目因为异常处理不到位,线上出了事故才临时补丁。
记住三点:
- 用标准异常类,别自己瞎造轮子
- 能标记
noexcept的地方尽量标记,编译器会感谢你 - 写代码时想清楚异常安全等级,至少做到「基本保证」
最后说一句:异常处理就像买保险——你希望永远用不上,但真出事的时候,你会庆幸自己买了。