核心要点速览

  • 标准库异常:以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_allocnew动态分配内存失败(系统内存耗尽)时自动抛出。
  • std::bad_castdynamic_cast向下转型失败(如基类指针转无关子类指针)时抛出。

使用细节(避坑要点)

  • what()返回值仅作调试参考,不可依赖格式(编译器实现不同)。
  • 逻辑错误需手动抛出(提示修复代码),运行时错误可手动抛出或系统触发。
  • 避免直接抛出std::logic_error/std::runtime_error基类,需用具体派生类(便于精准处理)。

二、自定义异常类

1. 设计原则

  1. 兼容性:继承std::exception或其派生类(支持统一捕获)。
  2. 信息完整:存储关键信息(如文件名、错误码、原因),便于定位问题。
  3. 安全性:what()必须加noexcept,构造 / 析构不可抛异常。
  4. 语义清晰:类名贴合业务(如FileException/NetworkException),避免模糊命名。

2. 正确实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
class FileException : public std::exception {
private:
std::string errorMsg; // 存储完整错误信息
public:
// 构造函数:拼接文件名+错误原因
FileException(const std::string& filename, const std::string& reason) {
errorMsg = "文件操作失败:文件名=" + filename + ",原因=" + reason;
}
// 重写what(),显式noexcept+override
const char* what() const noexcept override {
return errorMsg.c_str();
}
};

3. 常见错误

  • 漏加noexceptwhat()抛异常会触发双重异常,程序直接终止。
  • 未重写what():返回基类默认无意义信息,失去异常提示价值。
  • 滥用动态内存:异常类中new的资源需在析构函数(noexcept修饰)中释放,否则易泄漏。

4. 简化实现技巧

若业务场景可匹配标准异常子类,直接继承该子类,复用父类构造和what()

1
2
3
4
5
6
// 继承std::invalid_argument,复用父类逻辑
class InvalidAgeException : public std::invalid_argument {
public:
using std::invalid_argument::invalid_argument; // 复用构造函数
};
// 使用:throw InvalidAgeException("年龄必须在0-150之间,当前值:200");
写法 异常的 “标签” 调用者看到的信息
throw std::invalid_argument("年龄无效") 技术标签:参数无效 知道是参数错了,但不知道是哪个参数(年龄?手机号?密码?)
throw InvalidAgeException("年龄无效") 业务标签:年龄无效 一眼就知道是 “年龄” 这个业务字段出了问题

三、异常使用的最佳实践

1. 异常类型选择优先级

  1. 优先用标准库异常(避免重复造轮子,提升兼容性);
  2. 标准库无法覆盖时,自定义异常(遵循设计原则);
  3. 坚决杜绝基本类型异常(throw 1/throw "error",语义模糊,不支持多态捕获)。

2. 异常安全要求

  • 异常对象构造安全:构造函数不可抛异常,避免无法生成异常对象。
  • 资源清理依赖 RAII:用智能指针、资源守卫类管理try块中资源(文件句柄、锁、动态内存),防止泄漏。
  • 捕获粒度合理:先捕获具体异常(自定义 / 标准库派生类)→ 再用catch(const std::exception&)兜底 → 可选catch(...)(需记录日志,不吞异常)。

3. 避免滥用异常

  • 不用于控制流程:异常是 “错误处理机制”,不可替代if-else(如不用异常判断用户输入为空)。
  • 不抛笼统异常:避免直接抛std::exception,导致调用者无法区分具体错误。
  • 轻量场景用错误码:简单参数校验、非严重错误(如输入格式错),返回错误码更高效(避免异常栈展开开销)。

补充

  1. what()返回值生命周期:与异常对象一致,对象销毁后不可访问(避免悬垂指针)。
  2. 多态捕获:所有异常(标准 + 自定义)继承std::exception,故catch(const std::exception&)可统一捕获,是异常体系核心设计。
  3. 逻辑 / 运行时错误边界:编码阶段可通过逻辑避免→logic_error子类;编码阶段无法控制(如系统内存、文件是否存在)→runtime_error子类。