第34篇:C++ 异常处理机制:异常捕获、自定义异常与实战应用

一、学习目标与重点
- 掌握异常处理的核心概念(异常、抛出、捕获、处理)及基本语法
- 理解
try-catch-throw语句的执行流程,能够正确捕获和处理标准异常 - 学会自定义异常类,满足实际开发中的个性化异常场景需求
- 掌握异常处理的最佳实践,规避常见错误(内存泄漏、异常安全问题)
- 理解异常规格说明(C++11前)与
noexcept关键字的使用场景 - 结合实战案例,提升代码的健壮性和容错能力
💡 核心重点:try-catch 捕获规则、自定义异常的继承设计、异常安全保障、实战场景中的异常处理策略
二、异常处理概述
2.1 什么是异常处理
异常处理是C++中处理程序运行时错误的机制,核心是"将错误检测与错误处理分离"------在程序出错的地方(如除以零、内存分配失败)"抛出"异常,在合适的地方(如主函数、业务逻辑层)"捕获"并处理异常,避免程序直接崩溃,提升代码健壮性。
🗄️ 生活中的异常类比:
- 快递配送:快递员(程序执行)配送时发现地址错误(异常),不会直接丢弃快递,而是上报快递公司(抛出异常),由客服(异常处理模块)联系收件人解决(处理异常)
- 餐厅点餐:厨师(程序模块)发现食材耗尽(异常),不会拒绝出餐,而是告知服务员(抛出异常),由服务员(处理模块)向顾客说明并推荐其他菜品(处理异常)
2.2 为什么需要异常处理
在异常处理出现前,程序通常通过返回值判断是否出错,但存在明显缺陷:
cpp
// 传统错误处理:通过返回值判断(缺陷明显)
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可能是divide(-5,5)的合法结果) - 需手动检查每个函数返回值,代码冗余且易遗漏
- 错误传播困难(多层函数调用时,需逐层传递错误状态)
💡 异常处理的优势:
- 错误检测与处理分离,代码结构清晰
- 异常可跨函数、跨层级传播,无需逐层传递
- 可携带丰富的错误信息(如错误类型、原因、位置)
- 避免程序因小错误直接崩溃,提升用户体验
2.3 C++异常处理的核心组件
C++异常处理依赖三个核心关键字:
throw:抛出异常(检测到错误时,触发异常)try:尝试执行可能抛出异常的代码块(异常检测范围)catch:捕获并处理异常(匹配对应的异常类型,执行处理逻辑)
✅ 核心流程:try 块中执行代码 → 若发生错误,throw 抛出异常 → 程序跳转到最近的匹配 catch 块 → 执行 catch 中的处理逻辑 → 处理完成后,程序从 catch 块后继续执行
三、异常处理基本语法与执行流程
3.1 基本语法格式
cpp
try {
// 可能抛出异常的代码块
可能出错的操作;
if (错误条件) {
throw 异常值; // 抛出异常(异常值可是任意类型:int、string、自定义类等)
}
} catch (异常类型1 异常变量) {
// 处理异常类型1的逻辑
} catch (异常类型2 异常变量) {
// 处理异常类型2的逻辑
} catch (...) {
// 捕获所有未匹配的异常(兜底处理)
}
💡 语法解析:
try块:必须紧跟一个或多个catch块,不能单独存在throw表达式:可抛出任意类型的值(基本类型、字符串、自定义类),抛出后立即终止当前函数执行,跳转到匹配的catch块catch块:按顺序匹配异常类型,匹配成功则执行对应处理逻辑;catch (...)是万能捕获,需放在所有catch块最后- 异常变量:可选(如
catch (int)可省略变量名),用于获取抛出的异常信息
3.2 执行流程详解
💡 示例:基本异常处理流程(除数为0异常)
cpp
#include <iostream>
using namespace std;
int divide(int a, int b) {
if (b == 0) {
// 抛出异常:异常类型为string,携带错误信息
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) { // 捕获string类型异常
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块后继续执行(打印"程序继续执行...")
3.3 异常的匹配规则
catch 块按声明顺序匹配异常类型,匹配规则如下:
- 精确匹配:异常类型与
catch声明类型完全一致(如throw int(5)匹配catch (int)) - 派生类匹配:抛出的派生类异常可被基类类型的
catch块捕获(如自定义异常类继承自exception,可被catch (exception&)捕获) - 类型转换匹配:仅支持有限的隐式转换(如
char可转换为int,但int不能转换为double) catch (...)匹配所有未被前面catch块捕获的异常,必须放在最后
⚠️ 警告:catch 块的声明顺序至关重要,若将基类异常的 catch 块放在派生类之前,会导致派生类异常被基类 catch 块捕获,派生类的 catch 块永远无法执行:
cpp
// 错误示例:基类catch在前,派生类catch无法执行
class BaseException {};
class DerivedException : public BaseException {};
try {
throw DerivedException();
} catch (BaseException& e) { // 先匹配基类,派生类异常被捕获
cout << "基类异常" << endl;
} catch (DerivedException& e) { // 永远无法执行
cout << "派生类异常" << endl;
}
3.4 标准异常库
C++标准库提供了一系列预定义的异常类,均继承自 std::exception 基类,定义在 <exception> 头文件中,常用标准异常如下:
| 异常类 | 描述 | 适用场景 |
|---|---|---|
std::exception |
所有标准异常的基类 | 兜底捕获标准异常 |
std::logic_error |
逻辑错误(编译期可检测,但未避免) | 如无效参数、非法状态 |
std::invalid_argument |
无效参数错误 | 如向函数传递非法参数 |
std::out_of_range |
超出范围错误 | 如数组索引越界、string下标越界 |
std::runtime_error |
运行时错误(编译期无法检测) | 如除以零、文件打开失败 |
std::overflow_error |
溢出错误 | 如数值计算溢出 |
std::bad_alloc |
内存分配失败错误 | 如 new 分配内存失败 |
💡 示例:使用标准异常类
cpp
#include <iostream>
#include <exception> // 包含标准异常头文件
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3};
try {
// 尝试访问超出vector范围的元素(触发out_of_range异常)
cout << "访问索引3的元素:" << nums.at(3) << endl;
// at()方法会抛出out_of_range异常,[]运算符不会抛出异常
} catch (const out_of_range& e) {
// what()方法返回异常描述信息(继承自exception基类)
cout << "捕获到out_of_range异常:" << e.what() << endl;
} catch (const exception& e) { // 兜底捕获其他标准异常
cout << "捕获到标准异常:" << e.what() << endl;
}
try {
// 模拟内存分配失败(触发bad_alloc异常)
while (true) {
new int[1024 * 1024]; // 不断分配内存,直到耗尽
}
} catch (const bad_alloc& e) {
cout << "捕获到bad_alloc异常:" << e.what() << endl;
}
return 0;
}
✅ 运行结果:
捕获到out_of_range异常:vector::_M_range_check: __n (which is 3) >= this->size() (which is 3)
捕获到bad_alloc异常:std::bad_alloc
💡 技巧:标准异常类的 what() 方法返回C风格字符串(const char*),包含异常的简要描述,可用于日志输出或用户提示。
四、自定义异常类
标准异常类虽能满足常见场景,但实际开发中,我们常需要自定义异常(如业务相关的"用户不存在异常""权限不足异常"),自定义异常类需遵循以下原则:
4.1 自定义异常的设计原则
- 继承自标准异常类(推荐
std::exception或其派生类),便于统一捕获 - 重写
what()方法,返回自定义的异常描述信息 - 提供必要的构造函数(默认构造、带错误信息的构造)
- 异常类名清晰,体现异常类型(如
UserNotFoundException)
4.2 自定义异常类的实现
💡 示例:实现业务相关的自定义异常类
cpp
#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) {}
// 重写what()方法(必须是const noexcept,符合基类接口)
const char* what() const noexcept override {
return err_msg.c_str(); // 返回C风格字符串
}
};
// 2. 派生异常类:用户不存在异常
class UserNotFoundException : public BusinessException {
public:
// 构造函数:接收用户ID,拼接错误信息
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); // 抛出UserNotFoundException
} 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"); // 抛出PermissionDeniedException
} 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"无法执行"删除数据"操作
4.3 自定义异常的优势
- 语义清晰:异常类名直接体现异常类型,代码可读性更高
- 层次分明:通过继承关系组织异常(如业务异常→用户异常→权限异常),便于分类处理
- 信息丰富:可在构造函数中拼接详细的错误信息(如用户ID、操作名称),便于问题排查
- 兼容标准 :继承自
std::exception,可与标准异常一起捕获,统一异常处理逻辑
⚠️ 注意事项:
- 自定义异常类的
what()方法必须重写为const noexcept,符合std::exception基类的接口规范 - 异常类应尽量轻量,避免复杂的成员变量和构造逻辑(异常抛出时会拷贝异常对象)
- 优先使用引用捕获异常(
catch (const Exception& e)),避免拷贝开销,且支持多态匹配
五、异常处理的高级特性
5.1 异常规格说明(C++11前)与 noexcept
5.1.1 异常规格说明(已废弃)
C++11前,可通过 throw(类型列表) 声明函数可能抛出的异常类型,称为"异常规格说明":
cpp
// 声明该函数仅可能抛出int和string类型异常
void func() throw(int, string) {
// 函数体
}
// 声明该函数不抛出任何异常(等价于noexcept(true))
void func2() throw() {
// 函数体
}
⚠️ 缺陷:
- 若函数抛出了异常规格说明之外的异常,会调用
std::unexpected()函数,默认终止程序 - 编译期不强制检查,仅为程序员提供文档说明,实用性有限
- C++11已废弃该语法,推荐使用
noexcept关键字
5.1.2 noexcept 关键字(C++11及以上)
noexcept 用于声明函数是否可能抛出异常,语法更简洁、功能更明确:
cpp
// 声明函数不会抛出任何异常(推荐)
void func() noexcept {
// 函数体
}
// 声明函数可能抛出异常(等价于不写noexcept)
void func2() noexcept(false) {
// 函数体
}
// 条件式noexcept:当T的移动构造函数不抛出异常时,当前函数也不抛出
template <typename T>
void func3() noexcept(noexcept(T(std::move(T())))) {
// 函数体
}
💡 noexcept 的核心作用:
- 编译器优化 :若函数声明为
noexcept,编译器可省略异常处理相关的代码(如栈展开),提升性能 - 明确接口契约:告知调用者该函数无需处理异常,简化调用逻辑
- 影响标准库行为 :如
std::vector的push_back若元素的移动构造函数是noexcept,会使用移动语义(更高效),否则使用拷贝语义
⚠️ 警告:若 noexcept 函数实际抛出了异常,程序会调用 std::terminate() 终止,无法通过 try-catch 捕获,因此需确保 noexcept 函数确实不会抛出异常。
5.2 异常的传播与重新抛出
5.2.1 异常的跨函数传播
异常抛出后,若当前函数没有匹配的 catch 块,异常会向上传播到调用该函数的上层函数,直到找到匹配的 catch 块;若传播到 main 函数仍未捕获,程序会调用 std::terminate() 终止。
💡 示例:异常跨函数传播
cpp
#include <iostream>
#include <string>
using namespace std;
void func3() {
cout << "func3:抛出异常" << endl;
throw string("来自func3的异常");
}
void func2() {
cout << "func2:调用func3" << endl;
func3(); // 调用可能抛出异常的函数,自身无catch块
cout << "func2:执行完毕(不会执行)" << endl;
}
void func1() {
cout << "func1:调用func2" << endl;
try {
func2(); // 调用func2,可能传播异常
} catch (const int& e) {
cout << "func1:捕获int类型异常" << endl;
}
cout << "func1:执行完毕" << endl;
}
int main() {
cout << "main:调用func1" << endl;
try {
func1();
} catch (const string& e) { // 捕获从func3传播过来的string类型异常
cout << "main:捕获string类型异常:" << e << endl;
} catch (...) {
cout << "main:捕获未知异常" << endl;
}
cout << "main:程序结束" << endl;
return 0;
}
✅ 运行结果:
main:调用func1
func1:调用func2
func2:调用func3
func3:抛出异常
main:捕获string类型异常:来自func3的异常
main:程序结束
5.2.2 异常的重新抛出
有时需要在 catch 块中处理部分逻辑后,将异常重新抛出给上层函数处理,使用 throw; (不带参数)实现:
💡 示例:异常重新抛出
cpp
#include <iostream>
#include <string>
using namespace std;
void process_data(int data) {
if (data < 0) {
throw string("数据非法:负数不允许");
}
cout << "数据处理成功:" << data << endl;
}
void handle_request(int data) {
try {
process_data(data);
} catch (const string& e) {
// 局部处理:记录异常日志
cout << "日志记录:发生异常 - " << e << endl;
// 重新抛出异常,让上层函数处理
throw;
}
}
int main() {
try {
handle_request(-5);
} catch (const string& e) {
// 上层处理:提示用户
cout << "用户提示:操作失败,原因:" << e << endl;
}
return 0;
}
✅ 运行结果:
日志记录:发生异常 - 数据非法:负数不允许
用户提示:操作失败,原因:数据非法:负数不允许
⚠️ 注意事项:
throw;重新抛出的是原始异常对象,不会创建新的异常对象- 若在
catch块外使用throw;,会抛出std::bad_exception异常 - 重新抛出时,异常类型不变,上层函数需按原类型捕获
5.3 异常安全
异常安全是指程序抛出异常时,确保:
- 不会发生内存泄漏(已分配的内存被正确释放)
- 数据状态一致(不会出现部分修改的无效状态)
- 资源被正确释放(如文件句柄、网络连接、锁)
5.3.1 常见的异常安全问题
cpp
// 异常安全问题:内存泄漏
void unsafe_func() {
int* p = new int(10); // 分配内存
process_data(-5); // 可能抛出异常
delete p; // 若抛出异常,此句不执行,内存泄漏
}
5.3.2 异常安全的解决方案
- 使用智能指针 (推荐):智能指针(如
std::unique_ptr、std::shared_ptr)会在析构时自动释放内存,即使发生异常也不会泄漏 - 资源获取即初始化(RAII):将资源(如文件、锁)封装在类中,通过构造函数获取资源,析构函数释放资源,利用类的生命周期管理资源
- 使用容器和标准库组件 :标准库组件(如
vector、string)均具备异常安全性,避免手动管理资源
💡 示例:使用智能指针保证异常安全
cpp
#include <iostream>
#include <memory> // 包含智能指针头文件
#include <string>
using namespace std;
void safe_func() {
// 使用unique_ptr管理内存,自动释放
unique_ptr<int> p(new int(10));
// 模拟抛出异常
throw string("测试异常安全");
// 无需手动delete,智能指针析构时自动释放内存
}
int main() {
try {
safe_func();
} catch (const string& e) {
cout << "捕获异常:" << e << endl;
}
// 内存已被智能指针释放,无泄漏
return 0;
}
💡 示例:RAII模式管理文件资源
cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
// RAII类:管理文件资源
class FileGuard {
private:
ofstream file; // 文件流对象(资源)
public:
// 构造函数:获取资源(打开文件)
FileGuard(const string& filename) : file(filename) {
if (!file.is_open()) {
throw string("文件打开失败:" + filename);
}
cout << "文件打开成功:" << filename << endl;
}
// 析构函数:释放资源(关闭文件)
~FileGuard() {
if (file.is_open()) {
file.close();
cout << "文件关闭成功" << endl;
}
}
// 提供文件写入接口
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对象析构,文件自动关闭
}
int main() {
try {
write_file("test.txt", "Hello, 异常安全!");
} catch (const string& e) {
cout << "捕获异常:" << e << endl;
}
return 0;
}
✅ 运行结果:
文件打开成功:test.txt
文件关闭成功
捕获异常:模拟写入过程中异常
✅ 结论:即使在写入过程中抛出异常,FileGuard 对象会被析构,文件资源被正确释放,实现了异常安全。
六、异常处理的常见错误与最佳实践
6.1 常见错误
错误1:过度使用异常
将异常用于正常的控制流(如判断函数返回结果),导致代码效率降低、可读性变差:
cpp
// 错误示例:用异常处理正常逻辑
int find_element(const vector<int>& vec, int target) {
for (int i = 0; i < vec.size(); ++i) {
if (vec[i] == target) {
return i;
}
}
throw string("元素未找到"); // 不推荐:元素未找到是正常场景,非异常
}
错误2:捕获所有异常却不处理
使用 catch (...) 捕获所有异常,但未做任何处理,导致问题排查困难:
cpp
// 错误示例:捕获异常后忽略
try {
risky_operation();
} catch (...) {
// 无任何处理,异常被"吞掉"
}
错误3:抛出非异常类型的对象
抛出基本类型(如 int、double)或未继承自 std::exception 的自定义类,导致异常处理不统一:
cpp
// 不推荐:抛出int类型异常
void func() {
throw 5; // 异常类型不明确,难以统一处理
}
错误4:异常对象切片
按值捕获异常(catch (BaseException e)),而非按引用捕获,导致派生类异常的特有信息丢失:
cpp
// 错误示例:按值捕获导致切片
class DerivedException : public BaseException {
public:
const char* what() const noexcept override {
return "派生类异常";
}
};
try {
throw DerivedException();
} catch (BaseException e) { // 按值捕获,派生类对象被切片为基类对象
cout << e.what() << endl; // 输出基类的what()信息,而非派生类
}
6.2 最佳实践
实践1:明确异常使用场景
仅在"异常情况"(如内存分配失败、非法参数、IO错误)使用异常,正常控制流(如元素未找到、用户输入错误)使用返回值或其他方式处理。
实践2:优先使用标准异常或自定义异常类
- 系统级错误(如内存分配、数组越界)使用标准异常类
- 业务级错误(如用户不存在、权限不足)使用自定义异常类,且继承自
std::exception
实践3:按引用捕获异常
使用 catch (const Exception& e) 捕获异常,避免拷贝开销和对象切片,支持多态匹配。
实践4:合理组织 catch 块顺序
- 派生类异常的
catch块放在前面 - 基类异常的
catch块放在后面 catch (...)作为兜底,放在最后,并记录日志或终止程序
实践5:保证异常安全
- 使用智能指针和RAII模式管理资源,避免内存泄漏
- 重要操作(如数据库事务)需实现回滚机制,确保异常发生时数据状态一致
实践6:记录异常信息
捕获异常后,记录详细的异常信息(如异常类型、错误描述、发生位置、调用栈),便于问题排查。
实践7:避免在析构函数中抛出异常
析构函数若抛出异常,可能导致程序终止(如在栈展开过程中,析构函数抛出异常会调用 std::terminate()):
cpp
// 错误示例:析构函数抛出异常
class BadClass {
public:
~BadClass() {
throw string("析构函数异常"); // 危险!
}
};
七、实战案例:文件读写的异常处理
7.1 问题描述
实现一个文件读写工具类,支持读取文件内容和写入文件内容,要求:
- 处理文件操作中的常见异常(文件不存在、权限不足、磁盘已满等)
- 使用自定义异常类,提供详细的错误信息
- 保证异常安全(文件资源正确释放)
- 提供友好的用户提示和日志记录
7.2 实现思路
- 自定义文件相关异常类(继承自
std::exception):FileOpenException(文件打开失败)、FileReadException(文件读取失败)、FileWriteException(文件写入失败) - 基于RAII模式实现文件工具类
FileHandler,管理文件流资源 - 实现
read_file和write_file方法,抛出对应的自定义异常 - 在主函数中捕获异常,记录日志并提示用户
7.3 代码实现
cpp
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <exception>
using namespace std;
// 1. 自定义文件异常基类(继承自std::exception)
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);
}
// 检查是否读取失败(非EOF导致的失败)
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); // 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;
}
7.4 运行结果(正常情况)
日志:文件"input.txt"打开成功
日志:文件"input.txt"读取完成,共3行
文件内容:
[1] Hello, File Handling!
[2] This is a test file.
[3] C++ Exception Handling.
日志:文件"input.txt"关闭成功
日志:文件"output.txt"打开成功
日志:文件"output.txt"写入完成,共4行
日志:文件"output.txt"关闭成功
日志:文件"output.txt"打开成功
日志:文件"output.txt"读取完成,共4行
文件内容:
[1] === 新写入的内容 ===
[2] 原文件共3行
[3] 这是第一行新内容
[4] 这是第二行新内容
日志:文件"output.txt"关闭成功
7.5 异常情况测试(如input.txt不存在)
错误提示:文件操作异常:文件"input.txt", 原因:打开失败 - 无法打开文件(可能不存在或权限不足)
✅ 结论:该文件工具类通过自定义异常类提供了详细的错误信息,基于RAII模式保证了文件资源的正确释放,即使发生异常也不会导致资源泄漏,同时通过分层捕获异常,为用户提供了友好的提示,符合异常处理的最佳实践。
八、总结
- 异常处理是C++处理运行时错误的核心机制,通过
try-catch-throw实现错误检测与处理的分离,提升代码健壮性。 - 标准异常库提供了一系列预定义异常类(如
out_of_range、bad_alloc),自定义异常类应继承自std::exception,重写what()方法。 - 异常的匹配遵循精确匹配、派生类匹配规则,
catch块需按"派生类在前、基类在后"的顺序声明,catch (...)作为兜底。 - 异常安全是关键,需通过智能指针、RAII模式管理资源,避免内存泄漏和数据不一致。
- 最佳实践:明确异常使用场景、按引用捕获异常、记录异常信息、避免在析构函数中抛出异常。
通过本文学习,你应能熟练运用异常处理机制解决实际开发中的错误处理问题,编写健壮、可靠的C++代码。下一篇将深入探讨C++的输入输出流(IO流),包括文件IO、字符串IO等高级应用!