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

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

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

在这里插入图片描述

一、学习目标与重点

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

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

二、异常处理概述

2.1 什么是异常处理

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

🗄️ 生活中的异常类比:

  • 快递配送:快递员(程序执行)配送时发现地址错误(异常),不会直接丢弃快递,而是上报快递公司(抛出异常),由客服(异常处理模块)联系收件人解决(处理异常)
  • 餐厅点餐:厨师(程序模块)发现食材耗尽(异常),不会拒绝出餐,而是告知服务员(抛出异常),由服务员(处理模块)向顾客说明并推荐其他菜品(处理异常)

2.2 为什么需要异常处理

在异常处理出现前,程序通常通过返回值判断是否出错,但存在明显缺陷:

// 传统错误处理:通过返回值判断(缺陷明显)intdivide(int a,int b){if(b ==0){return-1;// 用-1表示错误,但-1可能是合法计算结果}return a / b;}intmain(){int result =divide(10,0);if(result ==-1){ cout <<"除数不能为0!"<< endl;// 依赖程序员主动检查返回值}else{ cout <<"结果:"<< result << endl;}return0;}

传统错误处理的缺陷:

  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 基本语法格式

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>usingnamespace std;intdivide(int a,int b){if(b ==0){// 抛出异常:异常类型为string,携带错误信息throwstring("错误:除数不能为0!");}return a / b;}intmain(){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;// 异常处理后,程序继续运行return0;}

✅ 运行结果:

尝试执行除法运算... 捕获到异常:错误:除数不能为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 块永远无法执行:

// 错误示例:基类catch在前,派生类catch无法执行classBaseException{};classDerivedException:publicBaseException{};try{throwDerivedException();}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>usingnamespace std;intmain(){ 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){newint[1024*1024];// 不断分配内存,直到耗尽}}catch(const bad_alloc& e){ cout <<"捕获到bad_alloc异常:"<< e.what()<< endl;}return0;}

✅ 运行结果:

捕获到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>usingnamespace std;// 1. 基础业务异常类(继承自std::exception)classBusinessException:publicexception{private: string err_msg;// 异常描述信息public:// 构造函数:带错误信息BusinessException(const string& msg):err_msg(msg){}// 重写what()方法(必须是const noexcept,符合基类接口)constchar*what()constnoexceptoverride{return err_msg.c_str();// 返回C风格字符串}};// 2. 派生异常类:用户不存在异常classUserNotFoundException:publicBusinessException{public:// 构造函数:接收用户ID,拼接错误信息UserNotFoundException(int user_id):BusinessException("用户不存在:ID="+to_string(user_id)){}};// 3. 派生异常类:权限不足异常classPermissionDeniedException:publicBusinessException{public:// 构造函数:接收用户名和操作,拼接错误信息PermissionDeniedException(const string& username,const string& operation):BusinessException("权限不足:用户\""+ username +"\"无法执行\""+ operation +"\"操作"){}};// 模拟业务函数:根据用户ID查询用户voidquery_user(int user_id){// 模拟用户不存在的场景if(user_id <1000|| user_id >9999){throwUserNotFoundException(user_id);// 抛出用户不存在异常} cout <<"查询成功:用户ID="<< user_id << endl;}// 模拟业务函数:执行敏感操作voidexecute_sensitive_operation(const string& username){// 模拟权限校验if(username !="admin"){throwPermissionDeniedException(username,"删除数据");// 抛出权限不足异常} cout <<"操作成功:用户\""<< username <<"\"执行删除数据操作"<< endl;}intmain(){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;}return0;}

✅ 运行结果:

业务异常:用户不存在: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类型异常voidfunc()throw(int, string){// 函数体}// 声明该函数不抛出任何异常(等价于noexcept(true))voidfunc2()throw(){// 函数体}

⚠️ 缺陷:

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

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

// 声明函数不会抛出任何异常(推荐)voidfunc()noexcept{// 函数体}// 声明函数可能抛出异常(等价于不写noexcept)voidfunc2()noexcept(false){// 函数体}// 条件式noexcept:当T的移动构造函数不抛出异常时,当前函数也不抛出template<typenameT>voidfunc3()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() 终止。

💡 示例:异常跨函数传播

#include<iostream>#include<string>usingnamespace std;voidfunc3(){ cout <<"func3:抛出异常"<< endl;throwstring("来自func3的异常");}voidfunc2(){ cout <<"func2:调用func3"<< endl;func3();// 调用可能抛出异常的函数,自身无catch块 cout <<"func2:执行完毕(不会执行)"<< endl;}voidfunc1(){ cout <<"func1:调用func2"<< endl;try{func2();// 调用func2,可能传播异常}catch(constint& e){ cout <<"func1:捕获int类型异常"<< endl;} cout <<"func1:执行完毕"<< endl;}intmain(){ cout <<"main:调用func1"<< endl;try{func1();}catch(const string& e){// 捕获从func3传播过来的string类型异常 cout <<"main:捕获string类型异常:"<< e << endl;}catch(...){ cout <<"main:捕获未知异常"<< endl;} cout <<"main:程序结束"<< endl;return0;}

✅ 运行结果:

main:调用func1 func1:调用func2 func2:调用func3 func3:抛出异常 main:捕获string类型异常:来自func3的异常 main:程序结束 
5.2.2 异常的重新抛出

有时需要在 catch 块中处理部分逻辑后,将异常重新抛出给上层函数处理,使用 throw; (不带参数)实现:

💡 示例:异常重新抛出

#include<iostream>#include<string>usingnamespace std;voidprocess_data(int data){if(data <0){throwstring("数据非法:负数不允许");} cout <<"数据处理成功:"<< data << endl;}voidhandle_request(int data){try{process_data(data);}catch(const string& e){// 局部处理:记录异常日志 cout <<"日志记录:发生异常 - "<< e << endl;// 重新抛出异常,让上层函数处理throw;}}intmain(){try{handle_request(-5);}catch(const string& e){// 上层处理:提示用户 cout <<"用户提示:操作失败,原因:"<< e << endl;}return0;}

✅ 运行结果:

日志记录:发生异常 - 数据非法:负数不允许 用户提示:操作失败,原因:数据非法:负数不允许 

⚠️ 注意事项:

  • throw; 重新抛出的是原始异常对象,不会创建新的异常对象
  • 若在 catch 块外使用 throw;,会抛出 std::bad_exception 异常
  • 重新抛出时,异常类型不变,上层函数需按原类型捕获

5.3 异常安全

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

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

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

#include<iostream>#include<memory>// 包含智能指针头文件#include<string>usingnamespace std;voidsafe_func(){// 使用unique_ptr管理内存,自动释放 unique_ptr<int>p(newint(10));// 模拟抛出异常throwstring("测试异常安全");// 无需手动delete,智能指针析构时自动释放内存}intmain(){try{safe_func();}catch(const string& e){ cout <<"捕获异常:"<< e << endl;}// 内存已被智能指针释放,无泄漏return0;}

💡 示例:RAII模式管理文件资源

#include<iostream>#include<fstream>#include<string>usingnamespace std;// RAII类:管理文件资源classFileGuard{private: ofstream file;// 文件流对象(资源)public:// 构造函数:获取资源(打开文件)FileGuard(const string& filename):file(filename){if(!file.is_open()){throwstring("文件打开失败:"+ filename);} cout <<"文件打开成功:"<< filename << endl;}// 析构函数:释放资源(关闭文件)~FileGuard(){if(file.is_open()){ file.close(); cout <<"文件关闭成功"<< endl;}}// 提供文件写入接口voidwrite(const string& content){ file << content << endl;}};voidwrite_file(const string& filename,const string& content){ FileGuard file(filename);// 构造时打开文件 file.write(content);throwstring("模拟写入过程中异常");// 抛出异常// 异常抛出后,FileGuard对象析构,文件自动关闭}intmain(){try{write_file("test.txt","Hello, 异常安全!");}catch(const string& e){ cout <<"捕获异常:"<< e << endl;}return0;}

✅ 运行结果:

文件打开成功:test.txt 文件关闭成功 捕获异常:模拟写入过程中异常 

✅ 结论:即使在写入过程中抛出异常,FileGuard 对象会被析构,文件资源被正确释放,实现了异常安全。

六、异常处理的常见错误与最佳实践

6.1 常见错误

错误1:过度使用异常

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

// 错误示例:用异常处理正常逻辑intfind_element(const vector<int>& vec,int target){for(int i =0; i < vec.size();++i){if(vec[i]== target){return i;}}throwstring("元素未找到");// 不推荐:元素未找到是正常场景,非异常}
错误2:捕获所有异常却不处理

使用 catch (...) 捕获所有异常,但未做任何处理,导致问题排查困难:

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

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

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

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

// 错误示例:按值捕获导致切片classDerivedException:publicBaseException{public:constchar*what()constnoexceptoverride{return"派生类异常";}};try{throwDerivedException();}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()):

// 错误示例:析构函数抛出异常classBadClass{public:~BadClass(){throwstring("析构函数异常");// 危险!}};

七、实战案例:文件读写的异常处理

7.1 问题描述

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

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

7.2 实现思路

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

7.3 代码实现

#include<iostream>#include<fstream>#include<string>#include<vector>#include<exception>usingnamespace std;// 1. 自定义文件异常基类(继承自std::exception)classFileException:publicexception{protected: string err_msg;// 异常描述信息public:FileException(const string& filename,const string& reason){ err_msg ="文件操作异常:文件\""+ filename +"\", 原因:"+ reason;}constchar*what()constnoexceptoverride{return err_msg.c_str();}};// 2. 派生异常:文件打开失败classFileOpenException:publicFileException{public:FileOpenException(const string& filename,const string& reason):FileException(filename,"打开失败 - "+ reason){}};// 3. 派生异常:文件读取失败classFileReadException:publicFileException{public:FileReadException(const string& filename,const string& reason):FileException(filename,"读取失败 - "+ reason){}};// 4. 派生异常:文件写入失败classFileWriteException:publicFileException{public:FileWriteException(const string& filename,const string& reason):FileException(filename,"写入失败 - "+ reason){}};// 5. 文件工具类(RAII模式)classFileHandler{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()){throwFileOpenException(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()){throwFileReadException(filename,"文件流状态异常");}// 逐行读取while(getline(file_stream, line)){ content.push_back(line);}// 检查是否读取失败(非EOF导致的失败)if(file_stream.bad()){throwFileReadException(filename,"读取过程中发生IO错误");} cout <<"日志:文件\""<< filename <<"\"读取完成,共"<< content.size()<<"行"<< endl;return content;}// 写入文件内容(覆盖写入)voidwrite_file(const vector<string>& content){// 检查写入状态if(!file_stream.good()){throwFileWriteException(filename,"文件流状态异常");}// 逐行写入for(const string& line : content){ file_stream << line << endl;// 检查写入是否成功if(file_stream.fail()){throwFileWriteException(filename,"写入数据失败(可能磁盘已满)");}}// 刷新缓冲区,确保数据写入磁盘 file_stream.flush();if(file_stream.fail()){throwFileWriteException(filename,"刷新缓冲区失败");} cout <<"日志:文件\""<< filename <<"\"写入完成,共"<< content.size()<<"行"<< endl;}};// 辅助函数:打印文件内容voidprint_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;}intmain(){ 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;}return0;}

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等高级应用!

Read more

C++效率掌握之STL库:unordered_map && unordered_set底层剖析

C++效率掌握之STL库:unordered_map && unordered_set底层剖析

文章目录 * 1.unordered_map、unordered_set的基本结构 * 2.普通迭代器 * 3.const迭代器 * 4.insert返回值 operator[] * 希望读者们多多三连支持 * 小编会继续更新 * 你们的鼓励就是我前进的动力! 看了前面的底层封装后,其实封装的过程及方法都大差不差,unordered_map && unordered_set 也是如此,所以本篇就简单提及一些细节,具体最详细的一些部分可以去看前面的文章 传送门:C++效率掌握之STL库:list底层剖析及迭代器万字详解 传送门:C++效率掌握之STL库:map && set底层剖析及迭代器万字详解 1.unordered_map、unordered_set的基本结构 🚩unordered_set: template<classK,classV>

By Ne0inhk
C++《set与map》

C++《set与map》

在之前我们已经学习了解了C++STL当中的string和vector等容器,现在我们已经懂得了这些容器提供的接口该如何使用,并且了解了这些容器的底层结构。接下来我们在本篇当中将继续学习STL内的容器set与map,在此这两个容器与我们之前学习的容器提供的成员函数以及底层结构有细微的差异。接下来就开始本篇的学习吧!!! 1.顺序式容器与关联式容器  在了解set与map之前我们要先来了解什么是顺序式容器、什么是关联式容器。 前面我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,比如交换⼀下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。 关联式容器也是用来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构,两个位置有紧密的关联关系,交换⼀下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unor

By Ne0inhk
C++ 函数重载:规则、实现与实战案例

C++ 函数重载:规则、实现与实战案例

C++ 函数重载:规则、实现与实战案例 💡 学习目标:掌握函数重载的核心规则,能够熟练实现重载函数,并解决实际开发中重载相关的常见问题。 💡 学习重点:函数重载的匹配原则、与默认参数的冲突处理、实战场景中的重载应用。 一、函数重载的定义与核心价值 ✅ 结论:函数重载是 C++ 多态性的基础体现,允许同一作用域内定义多个同名函数,通过参数列表的差异区分调用。 函数重载的核心价值在于: 1. 简化函数命名,避免为功能相似的函数创建不同名称,提升代码可读性 2. 适配不同类型或数量的参数输入,让函数调用更灵活 ⚠️ 注意事项:函数返回值不能作为区分重载函数的依据。 例如以下代码是非法的: #include<iostream>usingnamespace std;// 非法重载:仅返回值不同intadd(int a,int b){return a + b;}doubleadd(int a,int

By Ne0inhk
C++ STL map 系列全方位解析:从基础使用到实战进阶

C++ STL map 系列全方位解析:从基础使用到实战进阶

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一. map 核心概念:键值对与红黑树底层 * 1.1 什么是 map? * 1.2 关键类型定义 * 二. map 基础操作:构造、遍历与增删查改 * 2.1 构造与初始化 * 2.2 迭代器遍历 * 2.3 插入操作(insert) * 2.4 查找与删除(find/erase) * 2.5 核心特性:operator [] 的多功能性 * 三.

By Ne0inhk