C++ 异常处理机制:捕获、自定义与实战
核心目标
掌握异常处理的核心概念(异常、抛出、捕获、处理)及基本语法,理解 try-catch-throw 语句的执行流程。学会自定义异常类以满足实际开发中的个性化场景需求,同时掌握异常处理的最佳实践,规避内存泄漏和异常安全问题。此外,还需理解 noexcept 关键字的使用场景,结合实战案例提升代码的健壮性和容错能力。
异常处理概述
什么是异常处理
异常处理是 C++ 中处理程序运行时错误的机制,核心在于将错误检测与错误处理分离。在程序出错的地方(如除以零、内存分配失败)抛出异常,在合适的地方捕获并处理,避免程序直接崩溃,从而提升代码健壮性。
生活中的类比有助于理解:
- 快递配送:快递员发现地址错误时上报快递公司,由客服联系收件人解决。
- 餐厅点餐:厨师发现食材耗尽时告知服务员,由服务员向顾客说明并推荐其他菜品。
为什么需要异常处理
在异常处理出现前,程序通常通过返回值判断是否出错,但存在明显缺陷:
// 传统错误处理:通过返回值判断(缺陷明显)
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;
}
传统错误处理的缺陷包括:返回值可能与合法结果冲突;需手动检查每个函数返回值,代码冗余且易遗漏;错误传播困难,多层函数调用时需逐层传递错误状态。
异常处理的优势则体现在:错误检测与处理分离,代码结构清晰;异常可跨函数、跨层级传播,无需逐层传递;可携带丰富的错误信息;避免程序因小错误直接崩溃,提升用户体验。
C++ 异常处理的核心组件
C++ 异常处理依赖三个核心关键字:
throw:抛出异常(检测到错误时触发)。try:尝试执行可能抛出异常的代码块(异常检测范围)。catch:捕获并处理异常(匹配对应的异常类型,执行处理逻辑)。
核心流程为:try 块中执行代码 → 若发生错误,throw 抛出异常 → 程序跳转到最近的匹配 catch 块 → 执行 catch 中的处理逻辑 → 处理完成后,程序从 catch 块后继续执行。
异常处理基本语法与执行流程
基本语法格式
try {
// 可能抛出异常的代码块
// 可能出错的操作
if (错误条件) {
throw 异常值; // 抛出异常(异常值可是任意类型:int、string、自定义类等)
}
} catch (异常类型 1 异常变量) {
// 处理异常类型 1 的逻辑
} catch (异常类型 2 异常变量) {
// 处理异常类型 2 的逻辑
} catch (...) {
// 捕获所有未匹配的异常(兜底处理)
}
语法解析要点:
- try 块必须紧跟一个或多个 catch 块,不能单独存在。
- throw 表达式可抛出任意类型的值,抛出后立即终止当前函数执行,跳转到匹配的 catch 块。
- catch 块按顺序匹配异常类型,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(10, 0)。
- divide 函数检测到 b=0,throw 抛出 string 类型异常。
- 程序立即终止 try 块执行,跳转到 main 函数中最近的 catch 块。
- 第一个 catch 块匹配 string 类型异常,执行处理逻辑(打印错误信息)。
- catch 块执行完成后,程序从 catch 块后继续执行。
异常的匹配规则
catch 块按声明顺序匹配异常类型,规则如下:
- 精确匹配:异常类型与 catch 声明类型完全一致。
- 派生类匹配:抛出的派生类异常可被基类类型的 catch 块捕获。
- 类型转换匹配:仅支持有限的隐式转换。
- catch (...) 匹配所有未被前面 catch 块捕获的异常,必须放在最后。
注意:catch 块的声明顺序至关重要。若将基类异常的 catch 块放在派生类之前,会导致派生类异常被基类 catch 块捕获,派生类的 catch 块永远无法执行。
标准异常库
C++ 标准库提供了一系列预定义的异常类,均继承自 std::exception 基类,定义在 <exception> 头文件中。常用标准异常如下:
| 异常类 | 描述 | 适用场景 |
|---|---|---|
| std::exception | 所有标准异常的基类 | 兜底捕获标准异常 |
| std::logic_error | 逻辑错误 | 如无效参数、非法状态 |
| std::invalid_argument | 无效参数错误 | 如向函数传递非法参数 |
| std::out_of_range | 超出范围错误 | 如数组索引越界 |
| std::runtime_error | 运行时错误 | 如除以零、文件打开失败 |
| std::overflow_error | 溢出错误 | 如数值计算溢出 |
| std::bad_alloc | 内存分配失败错误 | 如 new 分配内存失败 |
使用标准异常类时,可以利用 what() 方法返回 C 风格字符串,包含异常的简要描述,可用于日志输出或用户提示。
自定义异常类
标准异常类虽能满足常见场景,但实际开发中,我们常需要自定义异常(如业务相关的'用户不存在异常''权限不足异常')。自定义异常类需遵循以下原则:继承自标准异常类(推荐 std::exception 或其派生类),重写 what() 方法,提供必要的构造函数,类名清晰体现异常类型。
自定义异常类的实现
以下示例实现了业务相关的自定义异常类:
#include <iostream>
#include <exception>
#include <string>
using namespace std;
// 1. 基础业务异常类(继承自 std::exception)
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();
}
};
// 2. 派生异常类:用户不存在异常
class UserNotFoundException : public BusinessException {
public:
UserNotFoundException(int user_id)
: BusinessException("用户不存在:ID=" + to_string(user_id)) {}
};
// 3. 派生异常类:权限不足异常
class PermissionDeniedException : public BusinessException {
public:
PermissionDeniedException(const string& username, const string& operation)
: BusinessException("权限不足:用户\"" + username + "\"无法执行\"" + operation + "\"操作") {}
};
// 模拟业务函数:根据用户 ID 查询用户
void query_user(int user_id) {
if (user_id < 1000 || user_id > 9999) {
throw UserNotFoundException(user_id);
}
cout << "查询成功:用户 ID=" << user_id << endl;
}
// 模拟业务函数:执行敏感操作
void execute_sensitive_operation(const string& username) {
if (username != "admin") {
throw PermissionDeniedException(username, "删除数据");
}
cout << "操作成功:用户\"" << username << "\"执行删除数据操作" << endl;
}
int main() {
try {
query_user(123);
} catch (const UserNotFoundException& e) {
cout << "业务异常:" << e.what() << endl;
} catch (const BusinessException& e) {
cout << "业务异常:" << e.what() << endl;
} catch (const exception& e) {
cout << "系统异常:" << e.what() << endl;
}
cout << endl;
try {
execute_sensitive_operation("test");
} catch (const PermissionDeniedException& e) {
cout << "业务异常:" << e.what() << endl;
} catch (const BusinessException& e) {
cout << "业务异常:" << e.what() << endl;
} catch (const exception& e) {
cout << "系统异常:" << e.what() << endl;
}
return 0;
}
运行结果为:
业务异常:用户不存在:ID=123
业务异常:权限不足:用户"test"无法执行"删除数据"操作
自定义异常的优势在于语义清晰、层次分明、信息丰富且兼容标准。注意事项包括:what() 方法必须重写为 const noexcept;异常类应尽量轻量;优先使用引用捕获异常以避免拷贝开销。
异常处理的高级特性
异常规格说明与 noexcept
C++11 前可通过 throw(类型列表) 声明函数可能抛出的异常类型,称为异常规格说明。但该语法已废弃,推荐使用 noexcept 关键字。
noexcept 用于声明函数是否可能抛出异常:
void func() noexcept;声明函数不会抛出任何异常。void func() noexcept(false);声明函数可能抛出异常(等价于不写)。
noexcept 的核心作用包括编译器优化(省略异常处理相关代码)、明确接口契约以及影响标准库行为(如 vector 的 push_back 若元素移动构造为 noexcept 会使用移动语义)。注意:若 noexcept 函数实际抛出了异常,程序会调用 std::terminate() 终止,无法通过 try-catch 捕获。
异常的传播与重新抛出
异常的跨函数传播:异常抛出后,若当前函数没有匹配的 catch 块,异常会向上传播到调用该函数的上层函数,直到找到匹配的 catch 块;若传播到 main 函数仍未捕获,程序会调用 std::terminate() 终止。
异常的重新抛出:有时需要在 catch 块中处理部分逻辑后,将异常重新抛出给上层函数处理,使用 throw;(不带参数)实现。这重新抛出的是原始异常对象,不会创建新的异常对象。
异常安全
异常安全是指程序抛出异常时,确保不会发生内存泄漏、数据状态一致、资源被正确释放。常见的解决方案包括使用智能指针、资源获取即初始化(RAII)模式以及使用容器和标准库组件。
例如,使用 unique_ptr 管理内存,即使发生异常也不会泄漏。RAII 模式则将资源封装在类中,利用析构函数自动释放资源,如 FileGuard 类管理文件流,确保异常发生时文件自动关闭。
异常处理的常见错误与最佳实践
常见错误
- 过度使用异常:将异常用于正常的控制流(如判断函数返回结果),导致代码效率降低。
- 捕获所有异常却不处理:使用 catch (...) 但未做任何处理,导致问题排查困难。
- 抛出非异常类型的对象:抛出基本类型或未继承自 std::exception 的自定义类,导致异常处理不统一。
- 异常对象切片:按值捕获异常而非按引用捕获,导致派生类异常的特有信息丢失。
最佳实践
- 明确异常使用场景:仅在异常情况(如内存分配失败、IO 错误)使用异常,正常控制流使用返回值。
- 优先使用标准异常或自定义异常类:系统级错误使用标准异常,业务级错误使用继承自 std::exception 的自定义异常。
- 按引用捕获异常:使用 catch (const Exception& e),避免拷贝开销和对象切片。
- 合理组织 catch 块顺序:派生类在前,基类在后,catch (...) 作为兜底。
- 保证异常安全:使用智能指针和 RAII 模式管理资源。
- 记录异常信息:捕获异常后记录详细信息便于排查。
- 避免在析构函数中抛出异常:可能导致程序终止。
实战案例:文件读写的异常处理
问题描述
实现一个文件读写工具类,支持读取和写入文件内容,要求处理文件操作中的常见异常,使用自定义异常类,保证异常安全,并提供友好的用户提示。
实现思路
- 自定义文件相关异常类(继承自 std::exception)。
- 基于 RAII 模式实现文件工具类 FileHandler,管理文件流资源。
- 实现 read_file 和 write_file 方法,抛出对应的自定义异常。
- 在主函数中捕获异常,记录日志并提示用户。
代码实现
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <exception>
using namespace std;
// 1. 自定义文件异常基类
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();
}
};
// 2. 派生异常:文件打开失败
class FileOpenException : public FileException {
public:
FileOpenException(const string& filename, const string& reason)
: FileException(filename, "打开失败 - " + reason) {}
};
// 3. 派生异常:文件读取失败
class FileReadException : public FileException {
public:
FileReadException(const string& filename, const string& reason)
: FileException(filename, "读取失败 - " + reason) {}
};
// 4. 派生异常:文件写入失败
class FileWriteException : public FileException {
public:
FileWriteException(const string& filename, const string& reason)
: FileException(filename, "写入失败 - " + reason) {}
};
// 5. 文件工具类(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 错误");
}
cout << "日志:文件\"" << filename << "\"读取完成,共" << content.size() << "行" << endl;
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, "刷新缓冲区失败");
}
cout << "日志:文件\"" << filename << "\"写入完成,共" << content.size() << "行" << endl;
}
};
// 辅助函数:打印文件内容
void print_file_content(const vector<string>& content) {
cout << "\n文件内容:" << endl;
for (int i = 0; i < content.size(); ++i) {
cout << "[" << i + 1 << "] " << content[i] << endl;
}
cout << endl;
}
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();
print_file_content(content);
vector<string> new_content = {"=== 新写入的内容 ===", "原文件共" + to_string(content.size()) + "行", "这是第一行新内容", "这是第二行新内容"};
FileHandler writer(write_filename, ios::out | ios::trunc);
writer.write_file(new_content);
FileHandler verify_reader(write_filename, ios::in);
vector<string> verify_content = verify_reader.read_file();
print_file_content(verify_content);
} catch (const FileOpenException& e) {
cout << "\n错误提示:" << e.what() << endl;
} catch (const FileReadException& e) {
cout << "\n错误提示:" << e.what() << endl;
} catch (const FileWriteException& e) {
cout << "\n错误提示:" << e.what() << endl;
} catch (const exception& e) {
cout << "\n系统错误:" << e.what() << endl;
} catch (...) {
cout << "\n未知错误:发生未预期的异常" << endl;
}
return 0;
}
结论
该文件工具类通过自定义异常类提供了详细的错误信息,基于 RAII 模式保证了文件资源的正确释放,即使发生异常也不会导致资源泄漏,同时通过分层捕获异常,为用户提供了友好的提示,符合异常处理的最佳实践。
总结
异常处理是 C++ 处理运行时错误的核心机制,通过 try-catch-throw 实现错误检测与处理的分离。标准异常库提供了一系列预定义异常类,自定义异常类应继承自 std::exception,重写 what() 方法。异常的匹配遵循精确匹配、派生类匹配规则,catch 块需按派生类在前、基类在后的顺序声明。异常安全是关键,需通过智能指针、RAII 模式管理资源。最佳实践包括明确异常使用场景、按引用捕获异常、记录异常信息、避免在析构函数中抛出异常。


