C++ 异常处理机制:异常捕获、自定义异常与实战应用
学习目标与重点
咱们今天聊聊 C++ 里怎么优雅地处理错误。核心目标就几个:搞懂 try-catch-throw 的底层逻辑,学会自己造异常类,还有在实际开发里怎么避免内存泄漏和状态不一致。特别是 noexcept 关键字和 RAII 模式,这可是提升代码健壮性的关键。
💡 核心重点:try-catch 的匹配顺序、自定义异常的继承设计、异常安全原则以及实战中的策略选择。
异常处理概述
什么是异常处理
简单说,异常处理就是把'发现错误'和'解决错误'分开。程序跑着跑着遇到坑(比如除以零),直接抛个异常出去,让专门负责兜底的模块去处理,而不是让程序直接崩掉。
🗄️ 打个比方:
- 快递配送:快递员发现地址错了(异常),不会把货扔路边,而是上报系统(抛出),客服介入联系收件人(处理)。
- 餐厅点餐:厨师发现没菜了,不直接拒单,告诉服务员(抛出),服务员跟顾客解释并推荐别的(处理)。
为什么需要异常处理
以前我们习惯用返回值判断错误,但这玩意儿缺陷挺多:
// 传统方式:通过返回值判断
int divide(int a, int b) {
if (b == 0) {
return -1; // 用 -1 表示错误,但 -1 可能是合法计算结果
}
return a / b;
}
int main() {
int result = divide(10, 0);
if (result == -1) {
cout << "除数不能为 0!" << endl;
} else {
cout << "结果:" << result << endl;
}
return 0;
}
这种写法有几个硬伤:返回值可能和正常结果冲突(比如 -1 本身是合法的),每层函数都得检查返回值,代码冗余还容易漏。异常处理的优势在于错误检测和处理分离,异常可以跨层级传播,还能携带丰富的错误信息。
C++ 异常处理的核心组件
主要靠三个关键字配合:
throw:检测到错时触发抛出。try:包裹可能出错的代码块。catch:捕获并处理特定类型的异常。
流程大概是:try 里执行 -> 出错 throw -> 跳到最近的匹配 catch -> 处理完继续往下走。
基本语法与执行流程
基本语法格式
try {
// 可能抛出异常的代码块
if (错误条件) {
throw 异常值; // 可以是 int、string 或自定义类
}
} catch (异常类型 1 异常变量) {
// 处理类型 1
} catch (异常类型 2 异常变量) {
// 处理类型 2
} catch (...) {
// 兜底捕获所有未匹配的异常
}
这里要注意,try 块后面必须紧跟至少一个 catch,不能单独存在。catch (...) 是万能捕获,记得放在最后,不然前面的会把它挡住。
执行流程详解
来看个具体的例子,模拟除数为 0 的情况:
#include <iostream>
using namespace std;
int divide(int a, int b) {
if (b == 0) {
throw string("错误:除数不能为 0!");
}
return a / b;
}
int main() {
int x = 10, y = 0;
try {
cout << "尝试执行除法运算..." << endl;
int result = divide(x, y);
cout << x << " / " << y << " = " << result << endl;
} catch (const string& err_msg) {
cout << "捕获到异常:" << err_msg << endl;
} catch (...) {
cout << "捕获到未知异常!" << endl;
}
cout << "程序继续执行..." << endl;
return 0;
}
运行后你会看到:
尝试执行除法运算...
捕获到异常:错误:除数不能为 0!
程序继续执行...
流程拆解一下:程序进 try 调用 divide,发现 b=0 就 throw 字符串,main 里的第一个 catch 匹配成功,打印信息,然后跳过后续代码,从 catch 块后面继续跑。
异常的匹配规则
catch 是按声明顺序匹配的。这里有几个坑:
- 精确匹配:类型完全一致。
- 派生类匹配:基类
catch能抓派生类异常。 - 顺序很重要:如果先把基类
catch放前面,派生类的catch永远拿不到机会。
// 错误示范:基类在前
try {
throw DerivedException();
} catch (BaseException& e) { // 先抓到基类,下面的 never run
cout << "基类异常" << endl;
} catch (DerivedException& e) {
cout << "派生类异常" << endl;
}
所以记住:派生类在前,基类在后。
标准异常库
C++ 标准库提供了一套现成的异常类,都在 <exception> 里,都继承自 std::exception。常用的有:
| 异常类 | 描述 | 适用场景 |
|---|---|---|
std::exception | 基类 | 兜底捕获 |
std::logic_error | 逻辑错误 | 无效参数、非法状态 |
std::invalid_argument | 无效参数 | 传参不对 |
std::out_of_range | 越界 | 数组下标、string 索引 |
std::runtime_error | 运行时错误 | 文件打开失败等 |
std::bad_alloc | 内存分配失败 | new 失败 |
#include <iostream>
#include <exception>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3};
try {
cout << nums.at(3) << endl; // at() 会抛异常
} catch (const out_of_range& e) {
cout << "捕获到 out_of_range 异常:" << e.what() << endl;
} catch (const exception& e) {
cout << "捕获到标准异常:" << e.what() << endl;
}
return 0;
}
技巧:what() 方法返回 C 风格字符串,适合做日志或提示。
自定义异常类
标准异常不够用时,就得自己写。比如业务上的'用户不存在'、'权限不足'。
设计原则
- 继承自
std::exception或其子类。 - 重写
what()方法返回描述。 - 构造函数带上错误信息。
- 类名要清晰,一眼看出是什么异常。
实现示例
#include <iostream>
#include <exception>
#include <string>
using namespace std;
// 基础业务异常
class BusinessException : public exception {
private:
string err_msg;
public:
BusinessException(const string& msg) : err_msg(msg) {}
const char* what() const noexcept override {
return err_msg.c_str();
}
};
// 派生:用户不存在
class UserNotFoundException : public BusinessException {
public:
UserNotFoundException(int user_id)
: BusinessException("用户不存在:ID=" + to_string(user_id)) {}
};
// 模拟业务函数
void query_user(int user_id) {
if (user_id < 1000 || user_id > 9999) {
throw UserNotFoundException(user_id);
}
cout << "查询成功:用户 ID=" << user_id << endl;
}
int main() {
try {
query_user(123);
} catch (const UserNotFoundException& e) {
cout << "业务异常:" << e.what() << endl;
} catch (const exception& e) {
cout << "系统异常:" << e.what() << endl;
}
return 0;
}
这样的好处是语义清晰,层次分明,而且兼容标准异常体系。
⚠️ 注意:what() 必须是 const noexcept,符合基类规范。异常对象尽量轻量,因为抛出时会拷贝。捕获时优先用引用 catch (const Exception& e)。
高级特性
noexcept 关键字
C++11 之前有 throw(...) 规格说明,但现在废了,推荐用 noexcept。
void func() noexcept { /* 保证不抛异常 */ }
void func2() noexcept(false) { /* 明确可能抛异常 */ }
作用有三:编译器优化(省栈展开代码)、明确接口契约、影响标准库行为(比如 vector 移动构造)。
⚠️ 警告:noexcept 函数如果真抛了异常,程序直接 terminate(),没法 catch。
异常传播与重新抛出
异常可以在函数间传播。如果当前层处理不了,可以用 throw; 重新抛给上层。
void process_data(int data) {
if (data < 0) throw string("数据非法");
}
void handle_request(int data) {
try {
process_data(data);
} catch (const string& e) {
cout << "记录日志:" << e << endl;
throw; // 重新抛出,保留原始对象
}
}
异常安全
核心就是:抛出异常时,别泄露内存,别留脏数据。
- 智能指针:
unique_ptr、shared_ptr自动管理内存。 - RAII:资源获取即初始化,析构时释放。
#include <memory>
#include <string>
void safe_func() {
unique_ptr<int> p(new int(10));
throw string("测试异常安全"); // 即使抛异常,p 也会析构
}
文件操作也是同理,封装成类,析构时关闭文件流。
常见错误与最佳实践
常见错误
- 过度使用:别把正常逻辑当异常处理(比如找元素没找到就抛异常)。
- 吞掉异常:
catch (...)什么都不干,问题排查会死得很惨。 - 按值捕获:
catch (BaseException e)会导致对象切片,丢失派生类信息。 - 析构抛异常:析构函数里别抛异常,否则可能导致程序终止。
最佳实践
- 异常情况才用异常,正常控制流用返回值。
- 优先用标准异常或继承
std::exception的自定义类。 - 按引用捕获 (
const &)。 catch顺序:派生类在前,基类在后,兜底在最后。- 重要操作要有回滚机制。
- 记录详细日志(类型、位置、堆栈)。
实战案例:文件读写
来个综合点的例子,做个文件工具类,支持读写,带异常处理和 RAII。
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <exception>
using namespace std;
// 自定义文件异常基类
class FileException : public exception {
protected:
string err_msg;
public:
FileException(const string& filename, const string& reason) {
err_msg = "文件操作异常:文件\"" + filename + "\", 原因:" + reason;
}
const char* what() const noexcept override {
return err_msg.c_str();
}
};
// 派生异常
class FileOpenException : public FileException {
public:
FileOpenException(const string& filename, const string& reason)
: FileException(filename, "打开失败 - " + reason) {}
};
class FileReadException : public FileException {
public:
FileReadException(const string& filename, const string& reason)
: FileException(filename, "读取失败 - " + reason) {}
};
class FileWriteException : public FileException {
public:
FileWriteException(const string& filename, const string& reason)
: FileException(filename, "写入失败 - " + reason) {}
};
// 文件工具类(RAII 模式)
class FileHandler {
private:
string filename;
fstream file_stream;
public:
FileHandler(const string& filename, ios_base::openmode mode)
: filename(filename) {
file_stream.open(filename, mode);
if (!file_stream.is_open()) {
throw FileOpenException(filename, "无法打开文件");
}
cout << "日志:文件\"" << filename << "\"打开成功" << endl;
}
~FileHandler() {
if (file_stream.is_open()) {
file_stream.close();
cout << "日志:文件\"" << filename << "\"关闭成功" << endl;
}
}
vector<string> read_file() {
vector<string> content;
string line;
if (!file_stream.good()) throw FileReadException(filename, "流状态异常");
while (getline(file_stream, line)) {
content.push_back(line);
}
if (file_stream.bad()) throw FileReadException(filename, "IO 错误");
return content;
}
void write_file(const vector<string>& content) {
if (!file_stream.good()) throw FileWriteException(filename, "流状态异常");
for (const string& line : content) {
file_stream << line << endl;
if (file_stream.fail()) throw FileWriteException(filename, "磁盘满");
}
file_stream.flush();
if (file_stream.fail()) throw FileWriteException(filename, "刷新失败");
}
};
int main() {
string read_filename = "input.txt";
string write_filename = "output.txt";
try {
FileHandler reader(read_filename, ios::in);
auto content = reader.read_file();
vector<string> new_content = {"新内容 1", "新内容 2"};
FileHandler writer(write_filename, ios::out | ios::trunc);
writer.write_file(new_content);
} catch (const FileOpenException& e) {
cout << "错误提示:" << e.what() << endl;
} catch (const FileReadException& e) {
cout << "错误提示:" << e.what() << endl;
} catch (const FileWriteException& e) {
cout << "错误提示:" << e.what() << endl;
} catch (const exception& e) {
cout << "系统错误:" << e.what() << endl;
}
return 0;
}
这个案例展示了如何通过分层异常类提供详细报错,利用 RAII 确保文件句柄在异常发生时也能正确关闭,避免了资源泄漏。
总结
异常处理是 C++ 健壮性的基石。通过 try-catch-throw 分离关注点,结合标准异常库和自定义类,再配合 noexcept 和 RAII 模式,能有效应对各种运行时错误。记住几个关键点:派生类 catch 在前,按引用捕获,资源管理交给智能指针或 RAII,别在析构函数里抛异常。把这些用好,你的代码质量能上一个大台阶。


