更新时间: 2026-08-27
54 篇讲了 throw 怎么扔、catch 怎么接。这篇回答下一个问题:该抛什么、接什么? 抛字符串字面量行不行?标准库的异常类有哪些?自定义异常怎么设计?
本文要回答:标准异常类的层级长什么样?什么时候用哪个?以及自定义异常类的正确姿势。
一、标准异常类层级
C++ 标准库把常用异常组织成一个继承体系,根是 std::exception:
@startuml
left to right direction
skinparam nodeFontSize 13
skinparam backgroundColor #FFFFFF
class "std::exception" as root #E8F1FF {
+ what() const
}
class "std::logic_error" as logic #DCE9FF
class "std::runtime_error" as runtime #DCE9FF
class "std::bad_alloc" as badalloc #FFF3D6
class "std::bad_cast" as badcast #FFF3D6
class "std::bad_variant_access" as badvar #FFF3D6
root <|-- logic
root <|-- runtime
root <|-- badalloc
root <|-- badcast
root <|-- badvar
note bottom of root
所有标准异常的基类,
what() 返回错误描述字符串
end note
@enduml再往下展开常用的子类:
| 类别 | 子类 | 触发场景 |
|---|---|---|
| 逻辑错误(程序 bug) | std::invalid_argument | 参数非法(如 stoi 传非数字) |
std::out_of_range | 越界(vector::at、string::at) | |
std::length_error | 长度超限(string 过长) | |
| 运行期错误(环境/输入) | std::runtime_error | 通用运行期错误 |
std::overflow_error | 算术溢出 | |
std::underflow_error | 算术下溢 | |
std::system_error | 系统调用错误(含 errno 包装) | |
| 分配/转换 | std::bad_alloc | new 分配失败 |
std::bad_cast | dynamic_cast 失败 | |
std::bad_variant_access | variant 取错类型(52 篇) |
关键区分:logic_error 表示"代码写错了"(参数非法、越界——本可以避免),runtime_error 表示"运行环境出了问题"(文件打不开、网络断了——写代码时无法预防)。这个区分对错误处理策略有指导意义:logic_error 通常在开发期暴露就该修,runtime_error 需要运行期容错。
二、what():错误信息
所有标准异常都有 what(),返回 const char* 的错误描述:
#include <stdexcept>
#include <iostream>
try {
throw std::runtime_error("磁盘写入失败: /dev/sda");
} catch (const std::exception& e) {
std::cout << "错误: " << e.what() << "\n";
}按 const std::exception& 捕获就能统一处理所有标准异常,e.what() 给人类可读的描述——日志、报错信息都靠它。what() 返回的指针只在异常对象存活期间有效,别把 what() 的结果长期保存(拷贝成 std::string 才安全)。
三、能 throw 任意类型,但别这么做
throw 可以抛任何东西——throw 42;、throw "error"; 都能编译:
throw 42; // 能编译,但 catch 要写 catch(int)
throw "something"; // 抛字符串字面量,catch 要写 catch(const char*)为什么不推荐抛非异常类型?
- 无法携带标准信息:没有
.what(),信息全靠自己约定 - catch 类型难写:调用者得记着"这个函数会抛 int",心智负担大
- 和标准库不统一:标准库都抛异常对象,你抛别的,兜底
catch (const std::exception&)接不住
惯例是抛标准异常类或自定义异常类。throw 后面的表达式其实还有讲究——C++11 起 throw e; 在传播时会调用移动或拷贝构造(把异常对象"复制"到异常存储区),所以抛局部对象是安全的(不会悬垂),但会多一次拷贝/移动,抛小对象无感。
四、自定义异常
标准异常不够用时(想带更多字段、特定的语义),继承 std::runtime_error 或 std::exception 自定义:
#include <stdexcept>
#include <string>
// 推荐:继承 runtime_error,复用 what() 的机制
class ConfigError : public std::runtime_error {
public:
explicit ConfigError(const std::string& file, const std::string& msg)
: std::runtime_error("配置文件 " + file + ": " + msg),
file_(file) {}
const std::string& file() const { return file_; }
private:
std::string file_;
};
// 使用
try {
// 某处:throw ConfigError("app.conf", "端口号非法");
} catch (const ConfigError& e) {
std::cerr << e.what() << "(文件: " << e.file() << ")\n";
} catch (const std::exception& e) {
std::cerr << "其它错误: " << e.what() << "\n";
}自定义异常的设计要点:
- 继承
std::runtime_error(运行期错误)或std::logic_error(逻辑错误),别直接继承std::exception——exception没有接受 string 的构造函数,你得自己存消息 - 构造函数里把消息拼好传给基类,
what()直接可用 - 需要的额外数据(文件名、错误码)做成成员 + getter
- 保持"小而专":一个异常类对应一类错误,别搞一个万能异常装所有情况
@startuml
left to right direction
skinparam nodeFontSize 13
skinparam backgroundColor #FFFFFF
class "std::exception" as root #E8F1FF {
+ what()
}
class "std::runtime_error" as rt #DCE9FF
class "ConfigError" as cfg #D9FFE2 {
+ file()
- file_
}
root <|-- rt
rt <|-- cfg
note bottom of cfg
自定义异常惯例:
继承 runtime_error,
构造函数拼消息,
额外字段做成成员
end note
@enduml五、异常与 C 错误码的互操作
C++ 代码调用 C 库(返回码 + errno)时,常在边界处转换:
#include <cerrno>
#include <cstring>
#include <stdexcept>
void read_file(const char* path) {
FILE* f = std::fopen(path, "r");
if (!f) {
// 把 errno 转成标准异常
throw std::system_error(errno, std::generic_category(),
std::string("打开失败: ") + path);
}
// ...
std::fclose(f);
}std::system_error(<system_error>)能把 errno 和错误信息一起包装,是 C++ 与 C 错误模型互操作的桥梁。反过来,C++ 异常想跨 C 边界传播也不行——C 函数没有异常机制,异常不能穿过 C 边界(会直接 terminate),所以跨边界要么全用返回码,要么在边界内 catch 掉并转成返回码。
六、C 对照
| 需求 | C | C++ |
|---|---|---|
| 通用错误 | errno + strerror(errno) | std::runtime_error + .what() |
| 参数错误 | 返回 EINVAL | std::invalid_argument |
| 越界 | 无标准(UB) | std::out_of_range |
| 分配失败 | malloc 返回 NULL | std::bad_alloc |
| 自定义错误 | 错误码枚举 | 自定义异常类 |
| 错误描述 | strerror(固定表) | what()(可携带任意上下文) |
C 的错误体系是"数字编码",错误描述靠查表;C++ 的异常是"对象",能带任意上下文信息。数字编码的优势是跨语言/跨进程传递方便(网络协议、系统调用层只能用错误码),对象异常的优势是表达力强。成熟工程往往两套都要——底层接口用错误码,应用层用异常。
七、与本站主线衔接
- 54 篇(try/catch):捕获语法,本篇补全"抛什么、接什么"
- 56 篇(栈展开):异常传播时 RAII 对象的析构顺序
- 57 篇(noexcept):哪些操作不该抛异常
- 51/52 篇(optional/variant):"不用异常的错误处理"替代方案
crash/:未捕获异常 → terminate → core dump,异常处理与崩溃排查的接口
八、一句话总结
标准异常以 std::exception 为根分两大枝——logic_error(代码 bug)与 runtime_error(环境问题),按 const std::exception& 捕获可统一处理、what() 取描述;业务需要时继承 runtime_error 自定义异常并携带上下文字段;牢记"别抛裸类型、异常不穿 C 边界"两条底线。