在程序开发中,错误处理是保障软件稳定性的关键环节。C 语言依赖错误码(如 errno)处理错误,但存在 “错误检测与处理耦合”“信息携带有限” 等问题;而 C++ 引入的异常处理机制,通过 throw、try、catch 关键字,实现了 “错误检测” 与 “错误处理” 的解耦,能携带更丰富的错误信息,是现代 C++ 工程的核心错误处理方式。本文将从异常的基本概念入手,逐步讲解异常的抛出与捕获、栈展开、异常安全等关键知识点,并结合工程案例展示如何设计健壮的异常体系。
一、异常的基本概念:为什么需要异常?
在理解异常的使用前,我们先明确其核心价值 —— 解决传统错误码的痛点:
- 解耦检测与处理:错误检测代码(如函数内部)只需抛出异常,无需关心谁来处理;错误处理代码(如调用链上层)只需捕获异常,无需关心错误在何处发生;
- 携带丰富信息:异常可抛出任意类型的对象(如自定义错误类),能包含错误描述、错误码、发生位置等信息,远超错误码的单一数值;
- 强制处理:未捕获的异常会终止程序,避免 “错误被忽略导致后续崩溃” 的问题(错误码可能被开发者遗漏检查)。
1.1 异常的核心术语
抛出(throw):当程序检测到错误时,通过 throw 语句引发一个异常(如 throw "Divide by zero");
捕获(catch):通过 catch 语句接收并处理异常,需指定匹配的异常类型;
监控(try):try 块包裹 “可能抛出异常的代码”,其后可跟多个 catch 块处理不同类型的异常;
栈展开(Stack Unwinding):抛出异常后,程序沿函数调用链向上查找匹配的 catch 块,途中会销毁所有局部对象,直到找到匹配的 catch 或终止程序。
二、异常的抛出与捕获:核心语法与流程
异常处理的基本语法结构为:
try {
// 可能抛出异常的代码块
risky_operation();
} catch (Type1& e) {
// 处理 Type1 类型的异常
} catch (Type2& e) {
// 处理 Type2 类型的异常
} catch (...) {
// 捕获所有未匹配的异常(兜底处理)
}
2.1 异常抛出:throw 语句
throw 语句用于引发异常,其后可跟任意类型的表达式(如字面量、自定义对象),但建议抛出对象而非指针(避免内存泄漏)。抛出后,throw 后的代码不再执行,程序直接进入 “栈展开” 阶段。
示例:除法函数抛出异常
#include <string>
using namespace std;
// 除法函数:当除数为 0 时抛出 string 类型的异常
double Divide(int a, int b) {
if (b == 0) {
// 抛出异常对象(会生成拷贝,避免局部对象销毁)
throw string("Divide by zero: 除数不能为 0");
}
return static_cast<double>(a) / b;
}
2.2 异常捕获:try-catch 块
try 块包裹 “可能抛出异常的代码”,catch 块按 “类型匹配” 原则处理异常。匹配规则如下:
- 精确匹配:
throw的对象类型与catch的参数类型完全一致(如throw string匹配catch (string&)); - 权限兼容:允许从 “非 const” 向 “const” 转换(如
throw string匹配catch (const string&)); - 继承兼容:允许从 “派生类” 向 “基类” 转换(如
throw SqlException匹配catch (Exception&),核心工程特性); - 兜底匹配:
catch (...)可捕获任意类型的异常,但无法获取异常信息,仅用于 “防止程序崩溃” 的兜底处理。
示例:多层函数调用的异常捕获
#include <iostream>
#include <string>
using namespace std;
// 下层函数:调用 Divide,可能抛出异常
void Func() {
int len, time;
cin >> len >> time;
// 调用可能抛出异常的 Divide
cout << "结果:" << Divide(len, time) << endl;
// 若 Divide 抛出异常,此句不会执行
cout << "Func 正常执行完毕" << endl;
}
// 上层函数(main):捕获异常
int main() {
while (1) { // 循环测试,避免程序退出
try {
Func(); // 监控 Func 的调用
}
// 匹配 string 类型的异常(精确匹配)
catch (const string& errmsg) {
cout << "捕获异常:" << errmsg << endl;
}
// 兜底捕获所有其他异常
catch (...) {
cout << "捕获未知异常" << endl;
}
cout << "------------------------" << endl;
}
return 0;
}
运行结果:
// 输入:10 0(除数为 0)
捕获异常:Divide by zero: 除数不能为 0
------------------------
// 输入:10 2(正常)
结果:5
Func 正常执行完毕
------------------------
2.3 栈展开:异常如何找到匹配的 catch?
当 throw 引发异常后,程序会按以下流程查找匹配的 catch 块,这个过程称为 “栈展开”:
- 检查当前函数:若
throw在try块内,查找当前try后的catch块,找到匹配的则执行; - 退出当前函数:若未找到匹配的
catch,则销毁当前函数的所有局部对象,退回到调用该函数的上层函数; - 重复查找:在上层函数中重复步骤 1-2,直到找到匹配的
catch; - 程序终止:若遍历到
main函数仍未找到匹配的catch,程序会调用terminate()函数终止(无法恢复)。
栈展开示意图(以 main -> Func -> Divide 调用链为例):
main() // 步骤 3:在 main 的 try 块后找到 catch(string&)
↓
Func() // 步骤 2:未找到 catch,销毁局部对象(len, time),退回到 main
↓
Divide() // 步骤 1:抛出 string 异常,无 try-catch,退回到 Func
三、工程级异常设计:基于继承的异常体系
在大型项目中,直接抛出 string、int 等基础类型的异常会导致 “异常类型混乱”“处理逻辑分散”。正确的做法是设计一套基于继承的异常体系:
- 定义一个基类
Exception,包含通用错误信息(如错误描述、错误码)和虚函数what()(用于返回错误详情); - 为不同模块定义派生类(如
SqlException、CacheException、HttpException),重写what()以携带模块特有的错误信息; - 捕获时只需捕获基类
Exception&,即可处理所有派生类异常,实现 “多态捕获”。
3.1 异常体系实现示例
#include <string>
#include <iostream>
using namespace std;
// 异常基类:所有自定义异常的父类
class Exception {
public:
Exception(const string& errmsg, int errid)
: _errmsg(errmsg), _errid(errid) {}
// 虚函数:返回错误详情(多态关键)
virtual string what() const {
return "错误ID:" + to_string(_errid) + ",错误信息:" + _errmsg;
}
// 获取错误ID(用于分类处理)
int get_errid() const { return _errid; }
protected:
string _errmsg; // 错误描述
int _errid; // 错误码(用于区分错误类型)
};
// 数据库模块异常(派生类)
class SqlException : public Exception {
public:
SqlException(const string& errmsg, int errid, const string& sql)
: Exception(errmsg, errid), _sql(sql) {}
// 重写 what(),添加 SQL 语句信息
virtual string what() const override {
string detail = "【SQL异常】";
detail += Exception::what();
detail += ",SQL语句:" + _sql;
return detail;
}
private:
string _sql; // 触发错误的 SQL 语句(模块特有信息)
};
// HTTP 模块异常(派生类)
class HttpException : public Exception {
public:
HttpException(const string& errmsg, int errid, const string& method)
: Exception(errmsg, errid), _method(method) {}
// 重写 what(),添加 HTTP 方法信息
virtual string what() const override {
string detail = "【HTTP异常】";
detail += Exception::what();
detail += ",请求方法:" + _method;
return detail;
}
private:
string _method; // HTTP 请求方法(GET/POST,模块特有信息)
};
3.2 异常体系的使用场景
基于继承的异常体系可轻松实现 “模块级错误分类处理”,例如:
// 模拟数据库操作:随机抛出 SqlException
void SQLMgr() {
if (rand() % 7 == 0) { // 1/7 概率触发异常
throw SqlException("权限不足,无法查询用户信息", 1001,
"select * from users where name = '张三'");
}
cout << "SQL 操作成功" << endl;
}
// 模拟 HTTP 服务:随机抛出 HttpException
void HttpServer() {
if (rand() % 3 == 0) { // 1/3 概率触发异常
throw HttpException("请求资源不存在", 2001, "GET");
}
cout << "HTTP 请求处理成功" << endl;
SQLMgr(); // 调用数据库模块,可能抛出 SqlException
}
int main() {
srand(time(0)); // 初始化随机数种子
while (1) {
this_thread::sleep_for(chrono::seconds(1)); // 每秒执行一次
try {
HttpServer();
}
// 捕获所有派生类异常(多态捕获)
catch (const Exception& e) {
cout << e.what() << endl;
// 可根据错误ID进行分类处理(如重试、告警)
if (e.get_errid() == 1001) {
cout << "提示:请检查数据库用户权限" << endl;
}
}
// 兜底捕获未知异常
catch (...) {
cout << "捕获未知异常,程序继续运行" << endl;
}
cout << "------------------------" << endl;
}
return 0;
}
运行结果:
【HTTP异常】错误ID:2001,错误信息:请求资源不存在,请求方法:GET
------------------------
HTTP 请求处理成功
【SQL异常】错误ID:1001,错误信息:权限不足,无法查询用户信息,SQL语句:select * from users where name = '张三'
提示:请检查数据库用户权限
------------------------
四、异常处理的关键进阶特性
4.1 异常重新抛出:分类处理与兜底
有时捕获异常后,需要对错误进行 “部分处理”,再将异常重新抛出给上层处理(如 “网络不稳定” 错误重试 3 次后仍失败,再向上抛出)。异常重新抛出使用 throw;(无参数),表示 “将当前捕获的异常原封不动抛出”。
示例:消息发送重试逻辑
// 底层发送函数:随机抛出 HttpException
void _SendMsg(const string& msg) {
if (rand() % 2 == 0) {
// 102 错误:网络不稳定(可重试)
throw HttpException("网络不稳定,发送失败", 102, "POST");
} else if (rand() % 7 == 0) {
// 103 错误:非好友(不可重试)
throw HttpException("对方不是你的好友,发送失败", 103, "POST");
}
cout << "消息发送成功:" << msg << endl;
}
// 上层发送函数:重试逻辑 + 异常重新抛出
void SendMsg(const string& msg) {
// 最多重试 3 次(共 4 次机会)
for (size_t i = 0; i < 4; ++i) {
try {
_SendMsg(msg);
return; // 发送成功,直接返回
} catch (const Exception& e) {
// 102 错误:网络不稳定,重试
if (e.get_errid() == 102) {
if (i == 3) { // 最后一次重试失败,重新抛出
cout << "重试 " << i + 1 << " 次后仍失败,放弃" << endl;
throw; // 重新抛出当前异常
}
cout << "开始第 " << i + 1 << " 次重试(网络不稳定)" << endl;
} else {
// 其他错误(如 103),直接重新抛出
throw;
}
}
}
}
// 调用 SendMsg 并处理异常
int main() {
srand(time(0));
string msg;
while (cin >> msg) {
try {
SendMsg(msg);
} catch (const Exception& e) {
cout << "最终错误:" << e.what() << endl;
}
cout << "------------------------" << endl;
}
return 0;
}
4.2 异常安全:避免资源泄漏
异常抛出后,程序会跳过后续代码直接进入栈展开,若此前申请了资源(如动态内存、文件句柄、锁),可能导致 “资源未释放” 的问题,这就是异常安全问题。
4.2.1 常见的异常安全风险
void Func() {
// 申请动态内存(资源)
int* arr = new int[10];
try {
int a, b;
cin >> a >> b;
// 若 b == 0,抛出异常,后续 delete 不会执行
cout << Divide(a, b) << endl;
} catch (...) {
// 未处理资源释放,直接重新抛出
throw;
}
// 若抛出异常,此句不会执行,内存泄漏
delete[] arr;
}
4.2.2 解决异常安全的两种方案
-
捕获异常后释放资源:在
catch块中释放资源,再重新抛出异常;void Func() { int* arr = new int[10]; try { int a, b; cin >> a >> b; cout << Divide(a, b) << endl; } catch (...) { // 释放资源后重新抛出 delete[] arr; throw; } // 正常执行时释放资源 delete[] arr; } -
使用 RAII 机制(推荐):通过 “资源获取即初始化”(如智能指针
unique_ptr、shared_ptr),让资源的生命周期与对象绑定,对象析构时自动释放资源(栈展开时会销毁局部对象)。#include <memory> // 智能指针头文件 void Func() { // 使用 unique_ptr,出作用域自动释放内存 unique_ptr<int[]> arr(new int[10]); int a, b; cin >> a >> b; // 即使抛出异常,arr 析构时也会释放内存 cout << Divide(a, b) << endl; }
注意:析构函数中禁止抛出异常!若析构函数抛出异常,可能导致 “资源释放到一半程序终止”(如释放 10 个资源时,第 5 个抛出异常,剩余 5 个未释放)。《Effective C++》明确指出:“别让异常逃离析构函数”。
4.3 异常规范:告知函数是否抛出异常
异常规范用于明确 “函数是否会抛出异常”,帮助开发者和编译器理解函数的错误行为。C++ 标准经历了两次调整:
4.3.1 C++98 异常规范(已废弃,不推荐)
通过 throw(类型列表) 声明函数可能抛出的异常类型:
throw():表示函数不会抛出任何异常;
throw(Type1, Type2):表示函数可能抛出 Type1 或 Type2 类型的异常。
缺点:过于繁琐,且编译器不会强制检查(声明 throw() 的函数仍可抛出异常,仅触发警告)。
4.3.2 C++11 异常规范(推荐)
通过 noexcept 简化声明:
noexcept:表示函数不会抛出任何异常;
不写 noexcept:表示函数可能抛出异常;
noexcept(表达式):作为运算符,判断表达式是否可能抛出异常(返回 bool)。
示例:
// 声明函数不会抛出异常
double Add(int a, int b) noexcept {
return a + b;
}
// 声明函数可能抛出异常(默认)
double Divide(int a, int b) {
if (b == 0) {
throw string("Divide by zero");
}
return static_cast<double>(a) / b;
}
int main() {
// noexcept 作为运算符:判断表达式是否可能抛出异常
cout << noexcept(Add(1, 2)) << endl; // 输出 1(不会抛出)
cout << noexcept(Divide(1, 0)) << endl;// 输出 0(可能抛出)
return 0;
}
注意:若 noexcept 修饰的函数抛出异常,程序会直接调用 terminate() 终止,无法通过 catch 捕获!
五、C++ 标准库的异常体系
C++ 标准库提供了一套预定义的异常体系,基类为 std::exception,定义在 <exception> 头文件中。常用派生类如下:
| 异常类 | 用途 |
|---|---|
std::bad_alloc | 内存分配失败(如 new 分配内存不足) |
std::bad_cast | 动态类型转换失败(如 dynamic_cast) |
std::out_of_range | 越界访问(如 vector::at() 越界) |
std::invalid_argument | 无效参数(如 stoi("abc")) |
std::logic_error | 逻辑错误(编译时可检测的错误) |
std::runtime_error | 运行时错误(编译时无法检测的错误) |
所有标准库异常类都重写了 what() 方法,返回错误描述字符串。
示例:使用标准库异常
#include <exception>
#include <vector>
#include <string>
using namespace std;
int main() {
try {
// 1. 内存分配失败(模拟)
vector<int> v(1000000000000); // 申请过大内存,抛出 bad_alloc
// 2. 越界访问
vector<int> v2 = {1, 2, 3};
cout << v2.at(10) << endl; // at() 越界,抛出 out_of_range
// 3. 无效参数
stoi("abc"); // 字符串转整数失败,抛出 invalid_argument
} catch (const bad_alloc& e) {
cout << "标准库异常:" << e.what() << endl; // 输出 "bad allocation"
} catch (const out_of_range& e) {
cout << "标准库异常:" << e.what() << endl; // 输出 "vector::_M_range_check: __n (which is 10) >= this->size() (which is 3)"
} catch (const exception& e) {
// 捕获所有标准库异常(多态)
cout << "标准库异常:" << e.what() << endl;
}
return 0;
}
六、异常处理的最佳实践
- 设计继承式异常体系:避免抛出基础类型(如
int、string),通过派生类携带模块特有信息,实现多态捕获; - 优先使用 RAII 保障异常安全:通过智能指针、锁对象(如
std::lock_guard)等 RAII 类型,自动释放资源,避免手动处理; - 慎用
catch(...):仅用于 “兜底防止程序崩溃”,捕获后应记录日志并尽可能恢复程序,而非直接忽略; - 析构函数不抛异常:若析构函数中可能发生错误(如关闭文件失败),应在内部捕获处理,避免异常逃离;
- 明确异常规范:对 “确定不会抛出异常” 的函数添加
noexcept(如getter、简单计算函数),帮助编译器优化; - 异常信息要完整:异常对象应包含 “错误码、错误描述、发生位置(如文件名、行号)、相关上下文(如 SQL 语句、请求参数)”,便于排查问题。
七、总结
C++ 异常处理机制是解决 “错误检测与处理解耦” 的核心方案,通过 throw、try、catch 实现灵活的错误传递,结合继承体系可构建工程级的异常处理系统。掌握异常的栈展开流程、异常安全保障、标准库异常体系,能显著提升代码的健壮性和可维护性。
1116

被折叠的 条评论
为什么被折叠?



