异常处理:标准库异常与自定义异常
核心要点速览
- 标准库异常:以
std::exception为基类,分逻辑错误(编译可避免)和运行时错误(运行不可预知)两大类,核心接口what()返回错误描述 - 自定义异常:推荐继承
std::exception,重写what()(必须加noexcept),语义需贴合业务场景 - 核心原则:优先复用标准库异常,自定义异常兼顾兼容性与信息完整性,杜绝基本类型异常,避免滥用
一、标准库异常体系(<stdexcept>)
结构
标准库提供了一套统一的异常类继承体系,所有标准异常均派生自std::exception抽象基类。这种设计保证了异常处理的兼容性 —— 无论捕获具体异常还是统一捕获基类引用,都能有效处理,是面试中 “规范异常使用” 的核心考点。
基类:std::exception
- 接口:
virtual const char* what() const noexcept;
纯虚函数,返回 C 风格错误描述字符串,子类必须重写该方法才能实例化。 - 特性:无参构造、拷贝构造、拷贝赋值均被标记为
noexcept,确保异常对象自身的构造和拷贝过程不会抛出新异常(避免双重异常)。
异常分类与常用派生类
标准异常按错误性质分为两大类,需精准区分适用场景, “错误类型与异常类的对应关系”:
1. 逻辑错误(std::logic_error)
- 特点:编译阶段可预判、可避免的错误(本质是编程逻辑缺陷),抛出这类异常通常意味着代码存在可修复的问题。
- 常用派生类及场景:
std::invalid_argument:参数无效,比如传递负数给 “年龄” 参数、空指针给需非空的函数。std::out_of_range:范围越界,比如数组索引超出大小、字符串substr的起始位置超出长度。std::domain_error:定义域错误,比如给平方根函数传入负数、数学运算的参数超出有效定义域。
2. 运行时错误(std::runtime_error)
- 特点:编译阶段无法预知,由运行环境或资源状态导致的错误(非编程逻辑问题),即使代码无缺陷也可能触发。
- 常用派生类及场景:
std::range_error:数值范围错误,比如计算结果溢出 / 下溢(如 int 类型相加超出最大值)。std::overflow_error:算术溢出,专门用于标记数值运算时的溢出场景(比range_error更精准)。
特殊标准异常(无需<stdexcept>)
std::bad_alloc:new动态分配内存失败(系统内存耗尽)时自动抛出。std::bad_cast:dynamic_cast向下转型失败(如基类指针转无关子类指针)时抛出。
使用细节(避坑要点)
what()返回值仅作调试参考,不可依赖格式(编译器实现不同)。- 逻辑错误需手动抛出(提示修复代码),运行时错误可手动抛出或系统触发。
- 避免直接抛出
std::logic_error/std::runtime_error基类,需用具体派生类(便于精准处理)。
二、自定义异常类
1. 设计原则
- 兼容性:继承
std::exception或其派生类(支持统一捕获)。 - 信息完整:存储关键信息(如文件名、错误码、原因),便于定位问题。
- 安全性:
what()必须加noexcept,构造 / 析构不可抛异常。 - 语义清晰:类名贴合业务(如
FileException/NetworkException),避免模糊命名。
2. 正确实现示例
1 | class FileException : public std::exception { |
3. 常见错误
- 漏加
noexcept:what()抛异常会触发双重异常,程序直接终止。 - 未重写
what():返回基类默认无意义信息,失去异常提示价值。 - 滥用动态内存:异常类中
new的资源需在析构函数(noexcept修饰)中释放,否则易泄漏。
4. 简化实现技巧
若业务场景可匹配标准异常子类,直接继承该子类,复用父类构造和what():
1 | // 继承std::invalid_argument,复用父类逻辑 |
| 写法 | 异常的 “标签” | 调用者看到的信息 |
|---|---|---|
throw std::invalid_argument("年龄无效") |
技术标签:参数无效 | 知道是参数错了,但不知道是哪个参数(年龄?手机号?密码?) |
throw InvalidAgeException("年龄无效") |
业务标签:年龄无效 | 一眼就知道是 “年龄” 这个业务字段出了问题 |
三、异常使用的最佳实践
1. 异常类型选择优先级
- 优先用标准库异常(避免重复造轮子,提升兼容性);
- 标准库无法覆盖时,自定义异常(遵循设计原则);
- 坚决杜绝基本类型异常(
throw 1/throw "error",语义模糊,不支持多态捕获)。
2. 异常安全要求
- 异常对象构造安全:构造函数不可抛异常,避免无法生成异常对象。
- 资源清理依赖 RAII:用智能指针、资源守卫类管理
try块中资源(文件句柄、锁、动态内存),防止泄漏。 - 捕获粒度合理:先捕获具体异常(自定义 / 标准库派生类)→ 再用
catch(const std::exception&)兜底 → 可选catch(...)(需记录日志,不吞异常)。
3. 避免滥用异常
- 不用于控制流程:异常是 “错误处理机制”,不可替代
if-else(如不用异常判断用户输入为空)。 - 不抛笼统异常:避免直接抛
std::exception,导致调用者无法区分具体错误。 - 轻量场景用错误码:简单参数校验、非严重错误(如输入格式错),返回错误码更高效(避免异常栈展开开销)。
补充
what()返回值生命周期:与异常对象一致,对象销毁后不可访问(避免悬垂指针)。- 多态捕获:所有异常(标准 + 自定义)继承
std::exception,故catch(const std::exception&)可统一捕获,是异常体系核心设计。 - 逻辑 / 运行时错误边界:编码阶段可通过逻辑避免→
logic_error子类;编码阶段无法控制(如系统内存、文件是否存在)→runtime_error子类。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 肖恩的博客!
评论

