C++ 异常处理机制:异常捕获、自定义异常与实战应用
导读
本文旨在深入探讨 C++ 中的异常处理机制。我们将掌握核心概念(异常、抛出、捕获、处理)及基本语法,理解 try-catch-throw 语句的执行流程,学会自定义异常类以满足实际开发需求,并掌握最佳实践以规避内存泄漏等常见错误。
学习目标
- 掌握异常处理的核心概念及基本语法
- 理解 try-catch-throw 语句的执行流程
- 学会自定义异常类,满足个性化场景需求
- 掌握异常处理的最佳实践,规避常见错误
- 理解 noexcept 关键字的使用场景
- 结合实战案例,提升代码的健壮性和容错能力
一、异常处理概述
1.1 什么是异常处理
异常处理是 C++ 中处理程序运行时错误的机制,核心是将错误检测与错误处理分离。在程序出错的地方(如除以零、内存分配失败)抛出异常,在合适的地方捕获并处理,避免程序直接崩溃。
生活中的类比有助于理解:
- 快递配送:快递员发现地址错误时上报快递公司,由客服联系收件人解决。
- 餐厅点餐:厨师发现食材耗尽时告知服务员,由服务员向顾客说明并推荐其他菜品。
1.2 为什么需要异常处理
在异常处理出现前,程序通常通过返回值判断是否出错,但存在明显缺陷:
// 传统错误处理:通过返回值判断
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.3 C++ 异常处理的核心组件
C++ 异常处理依赖三个核心关键字:
throw:抛出异常(检测到错误时触发)try:尝试执行可能抛出异常的代码块catch:捕获并处理异常
核心流程:try 块中执行代码 → 若发生错误,throw 抛出异常 → 程序跳转到最近的匹配 catch 块 → 执行处理逻辑 → 从 catch 块后继续执行。
二、异常处理基本语法与执行流程
2.1 基本语法格式
try {
// 可能抛出异常的代码块
if (错误条件) {
throw 异常值; // 抛出任意类型:int、string、自定义类等
}
} catch (异常类型 1 异常变量) {
// 处理异常类型 1 的逻辑
} catch (异常类型 2 异常变量) {
// 处理异常类型 2 的逻辑
} catch (...) {
// 捕获所有未匹配的异常(兜底处理)
}
try块必须紧跟一个或多个catch块。throw表达式抛出后立即终止当前函数执行。catch块按顺序匹配异常类型,catch (...)需放在最后。
2.2 执行流程详解
以下示例演示了基本的异常处理流程(除数为 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 抛出 string 异常 → 跳转至 main 函数中最近的 catch 块 → 打印错误信息 → 程序继续运行。
2.3 异常的匹配规则
catch 块按声明顺序匹配异常类型:
- 精确匹配:异常类型与 catch 声明类型完全一致。
- 派生类匹配:抛出的派生类异常可被基类类型的 catch 块捕获。
- 类型转换匹配:仅支持有限的隐式转换。
catch (...)匹配所有未被前面 catch 块捕获的异常,必须放在最后。
⚠️ 警告:catch 块的声明顺序至关重要。若将基类异常的 catch 块放在派生类之前,会导致派生类异常永远无法被执行。
2.4 标准异常库
C++ 标准库提供了一系列预定义的异常类,均继承自 std::exception 基类,定义在 <exception> 头文件中。
| 异常类 | 描述 | 适用场景 |
|---|---|---|
std::exception | 所有标准异常的基类 | 兜底捕获标准异常 |
std::logic_error | 逻辑错误 | 如无效参数、非法状态 |
std::invalid_argument | 无效参数错误 | 如向函数传递非法参数 |
std::out_of_range | 超出范围错误 | 如数组索引越界 |
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 << "访问索引 3 的元素:" << nums.at(3) << endl;
} 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 风格字符串,可用于日志输出或用户提示。
三、自定义异常类
标准异常类虽能满足常见场景,但实际开发中常需要自定义异常(如业务相关的'用户不存在异常')。
3.1 自定义异常的设计原则
- 继承自标准异常类(推荐
std::exception或其派生类)。 - 重写
what()方法,返回自定义的异常描述信息。 - 提供必要的构造函数。
- 异常类名清晰,体现异常类型。
3.2 自定义异常类的实现
以下示例实现了业务相关的自定义异常类:
#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。- 异常类应尽量轻量,避免复杂的成员变量。
- 优先使用引用捕获异常,避免拷贝开销。
四、异常处理的高级特性
4.1 异常规格说明与 noexcept
C++11 前可通过 throw(类型列表) 声明函数可能抛出的异常类型,但已废弃。推荐使用 noexcept 关键字。
void func() noexcept { /* 不会抛出异常 */ }
void func2() noexcept(false) { /* 可能抛出异常 */ }
noexcept 的核心作用:编译器优化(省略异常处理代码)、明确接口契约、影响标准库行为(如移动语义)。
⚠️ 警告:若 noexcept 函数实际抛出了异常,程序会调用 std::terminate() 终止。
4.2 异常的传播与重新抛出
4.2.1 异常的跨函数传播
异常抛出后,若当前函数没有匹配的 catch 块,异常会向上传播到调用该函数的上层函数。
4.2.2 异常的重新抛出
有时需要在 catch 块中处理部分逻辑后,将异常重新抛出给上层函数处理,使用 throw; 实现。
void handle_request(int data) {
try {
process_data(data);
} catch (const string& e) {
cout << "日志记录:发生异常 - " << e << endl;
throw; // 重新抛出原始异常对象
}
}
4.3 异常安全
异常安全是指程序抛出异常时,确保不发生内存泄漏、数据状态一致、资源被正确释放。
4.3.1 常见的异常安全问题
// 异常安全问题:内存泄漏
void unsafe_func() {
int* p = new int(10);
process_data(-5); // 可能抛出异常
delete p; // 若抛出异常,此句不执行,内存泄漏
}
4.3.2 异常安全的解决方案
- 使用智能指针:自动释放内存。
- RAII 模式:资源获取即初始化,利用生命周期管理资源。
- 使用容器和标准库组件:具备异常安全性。
示例:使用 RAII 模式管理文件资源。
#include <fstream>
using namespace std;
class FileGuard {
private:
ofstream file;
public:
FileGuard(const string& filename) : file(filename) {
if (!file.is_open()) {
throw string("文件打开失败:" + filename);
}
}
~FileGuard() {
if (file.is_open()) {
file.close();
}
}
void write(const string& content) {
file << content << endl;
}
};
void write_file(const string& filename, const string& content) {
FileGuard file(filename);
file.write(content);
throw string("模拟写入过程中异常");
// 异常抛出后,FileGuard 对象析构,文件自动关闭
}
五、常见错误与最佳实践
5.1 常见错误
- 过度使用异常:将异常用于正常的控制流。
- 捕获所有异常却不处理:导致问题排查困难。
- 抛出非异常类型的对象:导致异常处理不统一。
- 异常对象切片:按值捕获异常导致派生类特有信息丢失。
5.2 最佳实践
- 明确异常使用场景:仅在异常情况使用异常。
- 优先使用标准异常或自定义异常类:继承自
std::exception。 - 按引用捕获异常:避免拷贝开销和对象切片。
- 合理组织 catch 块顺序:派生类在前,基类在后。
- 保证异常安全:使用智能指针和 RAII 模式。
- 记录异常信息:便于问题排查。
- 避免在析构函数中抛出异常:可能导致程序终止。
六、实战案例:文件读写的异常处理
6.1 问题描述
实现一个文件读写工具类,要求处理文件操作中的常见异常,使用自定义异常类,保证异常安全,并提供友好的用户提示。
6.2 代码实现
#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) {}
};
// 文件工具类(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, "无法打开文件");
}
}
~FileHandler() {
if (file_stream.is_open()) {
file_stream.close();
}
}
vector<string> read_file() {
vector<string> content;
string line;
while (getline(file_stream, line)) {
content.push_back(line);
}
return content;
}
void write_file(const vector<string>& content) {
for (const string& line : content) {
file_stream << line << endl;
}
file_stream.flush();
}
};
int main() {
string read_filename = "input.txt";
string write_filename = "output.txt";
try {
FileHandler reader(read_filename, ios::in);
vector<string> content = reader.read_file();
FileHandler writer(write_filename, ios::out | ios::trunc);
vector<string> new_content = {"=== 新写入的内容 ===", "这是第一行新内容"};
writer.write_file(new_content);
} catch (const FileOpenException& e) {
cout << "错误提示:" << e.what() << endl;
} catch (const exception& e) {
cout << "系统错误:" << e.what() << endl;
}
return 0;
}
✅ 结论:该文件工具类通过自定义异常类提供了详细的错误信息,基于 RAII 模式保证了文件资源的正确释放,符合异常处理的最佳实践。
七、总结
- 异常处理是 C++ 处理运行时错误的核心机制,通过 try-catch-throw 实现错误检测与处理的分离。
- 标准异常库提供了一系列预定义异常类,自定义异常类应继承自
std::exception。 - 异常的匹配遵循精确匹配、派生类匹配规则,catch 块需按'派生类在前、基类在后'的顺序声明。
- 异常安全是关键,需通过智能指针、RAII 模式管理资源。
- 最佳实践包括明确异常使用场景、按引用捕获异常、记录异常信息等。
通过本文学习,你应能熟练运用异常处理机制解决实际开发中的错误处理问题,编写健壮、可靠的 C++ 代码。


