跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

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

介绍 C++ 异常处理机制,涵盖 try-catch-throw 语法、标准异常库使用及自定义异常类设计。通过实战案例演示文件读写中的异常安全处理,结合 RAII 模式与智能指针避免资源泄漏。同时讲解 noexcept 关键字、异常传播规则及最佳实践,帮助开发者提升代码健壮性与容错能力。

虚拟内存发布于 2026/3/29更新于 2026/9/1072 浏览
C++ 异常处理机制:异常捕获、自定义异常与实战应用

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

一、学习目标与重点

  • 掌握异常处理的核心概念(异常、抛出、捕获、处理)及基本语法
  • 理解 try-catch-throw 语句的执行流程,能够正确捕获和处理标准异常
  • 学会自定义异常类,满足实际开发中的个性化异常场景需求
  • 掌握异常处理的最佳实践,规避常见错误(内存泄漏、异常安全问题)
  • 理解异常规格说明(C++11 前)与 noexcept 关键字的使用场景
  • 结合实战案例,提升代码的健壮性和容错能力

核心重点:try-catch 捕获规则、自定义异常的继承设计、异常安全保障、实战场景中的异常处理策略

二、异常处理概述

2.1 什么是异常处理

异常处理是 C++ 中处理程序运行时错误的机制,核心是'将错误检测与错误处理分离'——在程序出错的地方(如除以零、内存分配失败)'抛出'异常,在合适的地方(如主函数、业务逻辑层)'捕获'并处理异常,避免程序直接崩溃,提升代码健壮性。

生活中的异常类比:

  • 快递配送:快递员(程序执行)配送时发现地址错误(异常),不会直接丢弃快递,而是上报快递公司(抛出异常),由客服(异常处理模块)联系收件人解决(处理异常)
  • 餐厅点餐:厨师(程序模块)发现食材耗尽(异常),不会拒绝出餐,而是告知服务员(抛出异常),由服务员(处理模块)向顾客说明并推荐其他菜品(处理异常)
2.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. 返回值可能与合法结果冲突(如上述 可能是 的合法结果)
-1
divide(-5,5)
  • 需手动检查每个函数返回值,代码冗余且易遗漏
  • 错误传播困难(多层函数调用时,需逐层传递错误状态)
  • 异常处理的优势:

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

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

    1. throw:抛出异常(检测到错误时,触发异常)
    2. try:尝试执行可能抛出异常的代码块(异常检测范围)
    3. catch:捕获并处理异常(匹配对应的异常类型,执行处理逻辑)

    核心流程:try 块中执行代码 → 若发生错误,throw 抛出异常 → 程序跳转到最近的匹配 catch 块 → 执行 catch 中的处理逻辑 → 处理完成后,程序从 catch 块后继续执行

    三、异常处理基本语法与执行流程

    3.1 基本语法格式
    try {
        // 可能抛出异常的代码块
        可能出错的操作;
        if (错误条件) {
            throw 异常值; // 抛出异常(异常值可是任意类型:int、string、自定义类等)
        }
    } catch (异常类型 1 异常变量) {
        // 处理异常类型 1 的逻辑
    } catch (异常类型 2 异常变量) {
        // 处理异常类型 2 的逻辑
    } catch (...) {
        // 捕获所有未匹配的异常(兜底处理)
    }
    

    语法解析:

    • try 块:必须紧跟一个或多个 catch 块,不能单独存在
    • throw 表达式:可抛出任意类型的值(基本类型、字符串、自定义类),抛出后立即终止当前函数执行,跳转到匹配的 catch 块
    • catch 块:按顺序匹配异常类型,匹配成功则执行对应处理逻辑;catch (...) 是万能捕获,需放在所有 catch 块最后
    • 异常变量:可选(如 catch (int) 可省略变量名),用于获取抛出的异常信息
    3.2 执行流程详解

    示例:基本异常处理流程(除数为 0 异常)

    #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=0,throw 抛出 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 块永远无法执行:

    // 错误示例:基类 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 分配内存失败

    示例:使用标准异常类

    #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 自定义异常类的实现

    示例:实现业务相关的自定义异常类

    #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(类型列表) 声明函数可能抛出的异常类型,称为'异常规格说明':

    // 声明该函数仅可能抛出 int 和 string 类型异常
    void func() throw(int, string) {
        // 函数体
    }
    
    // 声明该函数不抛出任何异常(等价于 noexcept(true))
    void func2() throw() {
        // 函数体
    }
    

    缺陷:

    • 若函数抛出了异常规格说明之外的异常,会调用 std::unexpected() 函数,默认终止程序
    • 编译期不强制检查,仅为程序员提供文档说明,实用性有限
    • C++11 已废弃该语法,推荐使用 noexcept 关键字
    5.1.2 noexcept 关键字(C++11 及以上)

    noexcept 用于声明函数是否可能抛出异常,语法更简洁、功能更明确:

    // 声明函数不会抛出任何异常(推荐)
    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::vector 的 push_back 若元素的移动构造函数是 noexcept,会使用移动语义(更高效),否则使用拷贝语义

    警告:若 noexcept 函数实际抛出了异常,程序会调用 std::terminate() 终止,无法通过 try-catch 捕获,因此需确保 noexcept 函数确实不会抛出异常。

    5.2 异常的传播与重新抛出
    5.2.1 异常的跨函数传播

    异常抛出后,若当前函数没有匹配的 catch 块,异常会向上传播到调用该函数的上层函数,直到找到匹配的 catch 块;若传播到 main 函数仍未捕获,程序会调用 std::terminate() 终止。

    示例:异常跨函数传播

    #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; (不带参数)实现:

    示例:异常重新抛出

    #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 常见的异常安全问题
    // 异常安全问题:内存泄漏
    void unsafe_func() {
        int* p = new int(10); // 分配内存
        process_data(-5);     // 可能抛出异常
        delete p;             // 若抛出异常,此句不执行,内存泄漏
    }
    
    5.3.2 异常安全的解决方案
    1. 使用智能指针(推荐):智能指针(如 std::unique_ptr、std::shared_ptr)会在析构时自动释放内存,即使发生异常也不会泄漏
    2. 资源获取即初始化(RAII):将资源(如文件、锁)封装在类中,通过构造函数获取资源,析构函数释放资源,利用类的生命周期管理资源
    3. 使用容器和标准库组件:标准库组件(如 vector、string)均具备异常安全性,避免手动管理资源

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

    #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 模式管理文件资源

    #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:过度使用异常

    将异常用于正常的控制流(如判断函数返回结果),导致代码效率降低、可读性变差:

    // 错误示例:用异常处理正常逻辑
    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 (...) 捕获所有异常,但未做任何处理,导致问题排查困难:

    // 错误示例:捕获异常后忽略
    try {
        risky_operation();
    } catch (...) {
        // 无任何处理,异常被'吞掉'
    }
    
    错误 3:抛出非异常类型的对象

    抛出基本类型(如 int、double)或未继承自 std::exception 的自定义类,导致异常处理不统一:

    // 不推荐:抛出 int 类型异常
    void func() {
        throw 5; // 异常类型不明确,难以统一处理
    }
    
    错误 4:异常对象切片

    按值捕获异常(catch (BaseException e)),而非按引用捕获,导致派生类异常的特有信息丢失:

    // 错误示例:按值捕获导致切片
    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()):

    // 错误示例:析构函数抛出异常
    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_file 和 write_file 方法,抛出对应的自定义异常
    4. 在主函数中捕获异常,记录日志并提示用户
    7.3 代码实现
    #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_range、bad_alloc),自定义异常类应继承自 std::exception,重写 what() 方法。
    3. 异常的匹配遵循精确匹配、派生类匹配规则,catch 块需按'派生类在前、基类在后'的顺序声明,catch (...) 作为兜底。
    4. 异常安全是关键,需通过智能指针、RAII 模式管理资源,避免内存泄漏和数据不一致。
    5. 最佳实践:明确异常使用场景、按引用捕获异常、记录异常信息、避免在析构函数中抛出异常。

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

    目录

    1. C++ 异常处理机制:异常捕获、自定义异常与实战应用
    2. 一、学习目标与重点
    3. 二、异常处理概述
    4. 2.1 什么是异常处理
    5. 2.2 为什么需要异常处理
    6. 2.3 C++ 异常处理的核心组件
    7. 三、异常处理基本语法与执行流程
    8. 3.1 基本语法格式
    9. 3.2 执行流程详解
    10. 3.3 异常的匹配规则
    11. 3.4 标准异常库
    12. 四、自定义异常类
    13. 4.1 自定义异常的设计原则
    14. 4.2 自定义异常类的实现
    15. 4.3 自定义异常的优势
    16. 五、异常处理的高级特性
    17. 5.1 异常规格说明(C++11 前)与 noexcept
    18. 5.1.1 异常规格说明(已废弃)
    19. 5.1.2 noexcept 关键字(C++11 及以上)
    20. 5.2 异常的传播与重新抛出
    21. 5.2.1 异常的跨函数传播
    22. 5.2.2 异常的重新抛出
    23. 5.3 异常安全
    24. 5.3.1 常见的异常安全问题
    25. 5.3.2 异常安全的解决方案
    26. 六、异常处理的常见错误与最佳实践
    27. 6.1 常见错误
    28. 错误 1:过度使用异常
    29. 错误 2:捕获所有异常却不处理
    30. 错误 3:抛出非异常类型的对象
    31. 错误 4:异常对象切片
    32. 6.2 最佳实践
    33. 实践 1:明确异常使用场景
    34. 实践 2:优先使用标准异常或自定义异常类
    35. 实践 3:按引用捕获异常
    36. 实践 4:合理组织 catch 块顺序
    37. 实践 5:保证异常安全
    38. 实践 6:记录异常信息
    39. 实践 7:避免在析构函数中抛出异常
    40. 七、实战案例:文件读写的异常处理
    41. 7.1 问题描述
    42. 7.2 实现思路
    43. 7.3 代码实现
    44. 7.4 运行结果(正常情况)
    45. 7.5 异常情况测试(如 input.txt 不存在)
    46. 八、总结

    更多推荐文章

    查看全部
    • LFM2.5-1.2B-Thinking 模型在 Ollama 上的多场景应用:写作、代码与学习
    • Admui 本地部署实战指南
    • 基于 DeepSeek API 的贪吃蛇游戏开发实战
    • Mac 应用安装提示已损坏的三种解决方法
    • Dify 工作流发布为 MCP Server 实践指南
    • MySQL 5.7 解压版安装与配置实战指南
    • 内网渗透实战:SSH 隧道搭建与 Web 代码审计漏洞利用
    • Llama-Factory 安装部署与依赖问题排查
    • 高精度加法算法实现详解
    • 网络安全基础入门:从零开始的学习路线与核心技能
    • AI 数据标注平台选型实践与效率提升技术逻辑
    • 文心一言开源模型部署及多维性能测评
    • 2025年AIGC市场:规模、趋势与挑战
    • Linux 基础指令介绍(上)
    • VSCode 集成 GitHub Copilot 使用指南
    • llama.cpp 量化大模型内网部署与运行实战
    • WebP格式处理一站式解决方案:让Photoshop完美支持现代图像格式
    • Python 逆向工程:从.pyc 字节码还原.py 源码
    • C++ 入门基础:历史渊源、应用场景与学习建议
    • Python 浏览器自动化:Playwright 结合 AI 的最佳实践

    相关免费在线工具

    • Base64 字符串编码/解码

      将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

    • Base64 文件转换器

      将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

    • Markdown转HTML

      将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

    • HTML转Markdown

      将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

    • JSON 压缩

      通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

    • JSON美化和格式化

      将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online