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

第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. 返回值可能与合法结果冲突(如上述 -1 可能是 divide(-5,5) 的合法结果)
  2. 需手动检查每个函数返回值,代码冗余且易遗漏
  3. 错误传播困难(多层函数调用时,需逐层传递错误状态)

💡 异常处理的优势:

  1. 错误检测与处理分离,代码结构清晰
  2. 异常可跨函数、跨层级传播,无需逐层传递
  3. 可携带丰富的错误信息(如错误类型、原因、位置)
  4. 避免程序因小错误直接崩溃,提升用户体验

2.3 C++异常处理的核心组件

C++异常处理依赖三个核心关键字:

  1. throw:抛出异常(检测到错误时,触发异常)
  2. try:尝试执行可能抛出异常的代码块(异常检测范围)
  3. 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!
程序继续执行...
执行流程拆解:
  1. 程序进入 try 块,执行 divide(10, 0)
  2. divide 函数检测到 b=0throw 抛出 string 类型异常
  3. 程序立即终止 try 块执行,跳转到 main 函数中最近的 catch
  4. 第一个 catch 块匹配 string 类型异常,执行处理逻辑(打印错误信息)
  5. catch 块执行完成后,程序从 catch 块后继续执行(打印"程序继续执行...")

3.3 异常的匹配规则

catch 块按声明顺序匹配异常类型,匹配规则如下:

  1. 精确匹配:异常类型与 catch 声明类型完全一致(如 throw int(5) 匹配 catch (int)
  2. 派生类匹配:抛出的派生类异常可被基类类型的 catch 块捕获(如自定义异常类继承自 exception,可被 catch (exception&) 捕获)
  3. 类型转换匹配:仅支持有限的隐式转换(如 char 可转换为 int,但 int 不能转换为 double
  4. 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 自定义异常的设计原则

  1. 继承自标准异常类(推荐 std::exception 或其派生类),便于统一捕获
  2. 重写 what() 方法,返回自定义的异常描述信息
  3. 提供必要的构造函数(默认构造、带错误信息的构造)
  4. 异常类名清晰,体现异常类型(如 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 自定义异常的优势

  1. 语义清晰:异常类名直接体现异常类型,代码可读性更高
  2. 层次分明:通过继承关系组织异常(如业务异常→用户异常→权限异常),便于分类处理
  3. 信息丰富:可在构造函数中拼接详细的错误信息(如用户ID、操作名称),便于问题排查
  4. 兼容标准 :继承自 std::exception,可与标准异常一起捕获,统一异常处理逻辑

⚠️ 注意事项:

  1. 自定义异常类的 what() 方法必须重写为 const noexcept,符合 std::exception 基类的接口规范
  2. 异常类应尽量轻量,避免复杂的成员变量和构造逻辑(异常抛出时会拷贝异常对象)
  3. 优先使用引用捕获异常(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 的核心作用:

  1. 编译器优化 :若函数声明为 noexcept,编译器可省略异常处理相关的代码(如栈展开),提升性能
  2. 明确接口契约:告知调用者该函数无需处理异常,简化调用逻辑
  3. 影响标准库行为 :如 std::vectorpush_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 异常安全

异常安全是指程序抛出异常时,确保:

  1. 不会发生内存泄漏(已分配的内存被正确释放)
  2. 数据状态一致(不会出现部分修改的无效状态)
  3. 资源被正确释放(如文件句柄、网络连接、锁)
5.3.1 常见的异常安全问题
cpp 复制代码
// 异常安全问题:内存泄漏
void unsafe_func() {
    int* p = new int(10); // 分配内存
    process_data(-5); // 可能抛出异常
    delete p; // 若抛出异常,此句不执行,内存泄漏
}
5.3.2 异常安全的解决方案
  1. 使用智能指针 (推荐):智能指针(如 std::unique_ptrstd::shared_ptr)会在析构时自动释放内存,即使发生异常也不会泄漏
  2. 资源获取即初始化(RAII):将资源(如文件、锁)封装在类中,通过构造函数获取资源,析构函数释放资源,利用类的生命周期管理资源
  3. 使用容器和标准库组件 :标准库组件(如 vectorstring)均具备异常安全性,避免手动管理资源

💡 示例:使用智能指针保证异常安全

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:抛出非异常类型的对象

抛出基本类型(如 intdouble)或未继承自 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 问题描述

实现一个文件读写工具类,支持读取文件内容和写入文件内容,要求:

  1. 处理文件操作中的常见异常(文件不存在、权限不足、磁盘已满等)
  2. 使用自定义异常类,提供详细的错误信息
  3. 保证异常安全(文件资源正确释放)
  4. 提供友好的用户提示和日志记录

7.2 实现思路

  1. 自定义文件相关异常类(继承自 std::exception):FileOpenException(文件打开失败)、FileReadException(文件读取失败)、FileWriteException(文件写入失败)
  2. 基于RAII模式实现文件工具类 FileHandler,管理文件流资源
  3. 实现 read_filewrite_file 方法,抛出对应的自定义异常
  4. 在主函数中捕获异常,记录日志并提示用户

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模式保证了文件资源的正确释放,即使发生异常也不会导致资源泄漏,同时通过分层捕获异常,为用户提供了友好的提示,符合异常处理的最佳实践。

八、总结

  1. 异常处理是C++处理运行时错误的核心机制,通过 try-catch-throw 实现错误检测与处理的分离,提升代码健壮性。
  2. 标准异常库提供了一系列预定义异常类(如 out_of_rangebad_alloc),自定义异常类应继承自 std::exception,重写 what() 方法。
  3. 异常的匹配遵循精确匹配、派生类匹配规则,catch 块需按"派生类在前、基类在后"的顺序声明,catch (...) 作为兜底。
  4. 异常安全是关键,需通过智能指针、RAII模式管理资源,避免内存泄漏和数据不一致。
  5. 最佳实践:明确异常使用场景、按引用捕获异常、记录异常信息、避免在析构函数中抛出异常。

通过本文学习,你应能熟练运用异常处理机制解决实际开发中的错误处理问题,编写健壮、可靠的C++代码。下一篇将深入探讨C++的输入输出流(IO流),包括文件IO、字符串IO等高级应用!

相关推荐
郝学胜-神的一滴2 小时前
C++备忘录模式:优雅实现对象状态保存与恢复
开发语言·c++·程序人生·备忘录模式
小年糕是糕手2 小时前
【C++】string类(三)
开发语言·数据结构·c++·程序人生·算法
星河耀银海2 小时前
C++ 继承:面向对象的代码复用核心机制
开发语言·c++
AndroidCode2 小时前
Android Automotive Power Policy 全流程技术解析
android
longzhen9z2 小时前
SpringSecurity踢出指定用户
java
胖祥2 小时前
onnx之NodeComputeInfo
开发语言·c++·算法
aykon2 小时前
android 扫码优化方案
android
墨狂之逸才2 小时前
Android TV 智能看板开发踩坑指南:WebView 常见问题与解决方
android
林栩link2 小时前
Now in Android 现代应用开发实践(三):架构设计(UI)
android·android jetpack