24、异常处理:异常机制、try-catch-throw、标准异常类、自定义异常、异常安全
异常机制:为什么我们需要它?
写代码这么多年,我越来越觉得,异常处理就像汽车的保险带。平时你可能觉得它碍事,但真撞车的时候,它能救命。
你想想看,传统的错误处理方式是什么?函数返回一个错误码,调用方检查这个码。这有什么问题?
- 容易忘:程序员一忙起来,很容易忘了检查返回值。然后程序带着错误往下跑,结果越跑越偏。
- 代码臃肿:真正的业务逻辑被一堆 if-else 错误检查给淹没了。读代码就像在垃圾堆里找宝藏。
- 构造函数没法返回错误码:构造函数没有返回值,你总不能让它返回一个 int 吧?
异常机制就是为了解决这些问题。它把「正常逻辑」和「错误处理」分开了。说白了,就是让代码更干净,更安全。
核心思想:异常发生的时候,程序会沿着调用栈向上跳,直到找到能处理这个异常的 catch 块。如果一直找不到,程序就崩溃。
try-catch-throw:三板斧
这三个关键字,是异常处理的基石。我习惯把它们叫做「三板斧」:
- throw:抛出异常。就像你发现火情,拉响了警报。
- try:监控一段可能出问题的代码。就像你在火警区域巡逻。
- 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;
}
运行结果会直接打印出「捕获到异常: 除数不能为零!」。程序不会崩溃,而是优雅地处理了这个错误。
我的习惯:catch 的时候,尽量用引用捕获(比如 const std::exception&),这样可以避免对象切片,也能保留多态信息。
标准异常类:C++ 给你搭好的架子
C++ 标准库已经给我们准备了一套异常类,都定义在 <stdexcept> 头文件里。它们都继承自 std::exception。
我整理了一张表,方便你查阅:
| 异常类 | 说明 | 常见场景 |
|---|---|---|
std::runtime_error |
运行时错误 | 除零、文件打不开 |
std::logic_error |
逻辑错误 | 参数越界、状态非法 |
std::invalid_argument |
无效参数 | 传入了不合法的值 |
std::out_of_range |
越界访问 | vector 的 at() 方法 |
std::bad_alloc |
内存分配失败 | new 申请不到内存 |
我个人建议,能用标准异常就别自己造轮子。标准异常已经覆盖了 90% 的常见场景,而且大家都认识它,可读性更好。
自定义异常:当标准库不够用的时候
有一次我在项目中写一个网络库,需要传递「连接超时」和「DNS 解析失败」两种不同的错误。标准异常里没有这么细的类,怎么办?自己写一个。
自定义异常很简单,继承 std::exception,重写 what() 方法就行:
#include <iostream>
#include <exception>
#include <string>
class NetworkException : public std::exception {
public:
explicit NetworkException(const std::string& msg) : message(msg) {}
const char* what() const noexcept override {
return message.c_str();
}
private:
std::string message;
};
void connectToServer() {
// 模拟连接失败
throw NetworkException("连接超时,请检查网络");
}
int main() {
try {
connectToServer();
} catch (const NetworkException& e) {
std::cerr << "网络错误: " << e.what() << std::endl;
}
return 0;
}
注意:重写 what() 时,一定要加上 noexcept。因为 std::exception::what() 是 noexcept 的,如果你不加,编译器可能会报 warning,甚至在某些实现下行为未定义。
异常安全:写代码的底线
异常安全,说白了就是:当异常发生时,你的程序不会留下烂摊子。比如内存泄漏、文件句柄没关、数据状态不一致。
我把它分为三个等级:
- 基本保证:异常发生后,程序处于有效状态,没有资源泄漏。但数据可能不是原来的样子。
- 强保证:异常发生后,程序状态回滚到调用之前。就像什么都没发生过。
- 不抛异常保证:这个操作绝对不会抛出异常。比如
int的拷贝赋值。
嗯,这里要注意,强保证很难做到,但基本保证是底线。我曾经在一个项目中,因为没注意异常安全,导致数据库连接池泄漏,线上服务每跑 48 小时就得重启一次。后来排查了三天才找到原因——一个 new 操作抛了异常,导致连接没归还。
怎么做到异常安全?记住三个原则:
- RAII:资源获取即初始化。用智能指针管理内存,用
std::lock_guard管理锁。这样异常发生时,析构函数会自动释放资源。 - 尽量少在构造函数里抛异常:构造函数抛异常,对象就没构造完成,析构函数不会被调用。如果你在构造函数里 new 了东西,得自己清理。
- 使用 copy-and-swap:先拷贝一份,修改拷贝,如果修改成功,再和原对象交换。这样即使修改过程抛异常,原对象纹丝不动。
一句话总结:异常安全不是锦上添花,而是雪中送炭。它决定了你的程序在出问题时,是优雅降级,还是直接爆炸。
知识体系总览
下面这张图,帮你把本章的知识点串起来。你可以把它当作一个思维导图来看:
这张图把异常处理的四个核心模块串在了一起。你可以看到,异常安全是最终的落脚点——前面学的一切,都是为了写出更安全的代码。
好了,关于异常处理,我们就聊到这里。记住,异常不是洪水猛兽,用好了,它能让你的代码更健壮、更优雅。下次写代码的时候,不妨多想想:如果这里抛异常了,我的程序能扛得住吗?