C++ 异常完全指南:从语法到实战,优雅处理程序错误

C++ 异常完全指南:从语法到实战,优雅处理程序错误
在这里插入图片描述

🔥草莓熊Lotso:
❄️个人专栏:
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:

在这里插入图片描述

文章目录


前言:

在 C 语言中,我们通过错误码处理异常,但错误码只能返回简单状态,无法携带详细错误信息,且需要手动逐层检查,繁琐且易遗漏。C++ 的异常机制则彻底改变了这一现状 —— 它将 “错误检测” 与 “错误处理” 分离,允许程序在出错时抛出异常对象(携带完整错误信息),在合适的位置捕获并处理,让代码更优雅、逻辑更清晰。本文结合核心知识点和代码,从异常的基本语法、栈展开机制、捕获匹配规则,到异常安全、标准库异常体系,再到实战案例,全方位拆解 C++ 异常,帮你从 “会用” 到 “用好”,应对大型项目的错误处理需求。

一. 异常的核心概念与基本语法\

  • 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分,检测环节无需知道问题的处理模块的所有细节
  • C语言主要通过错误码的形式处理错误,错误码的本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦。而异常是抛出一个对象,这个对象可以函数更全面的拿到各种信息。

1.1 异常的核心思想

  • 抛出(throw):程序遇到错误时,通过throw抛出一个异常对象(可是任意类型,推荐自定义异常类);
  • 捕获(catch):通过catch语句捕获指定类型的异常,执行对应的处理逻辑;
  • try 块try包裹可能抛出异常的代码,后续紧跟一个或多个catch块,用于匹配异常

分析

  • 程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前调用链决定了应该由那个catch的处理代码来处理该异常。
  • 被选中的处理代码是调用链中与该对象类型匹配且抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误。
  • 当 throw 执行时,throw 后面的语句将不再被执行。程序的执行从throw位置跳到与之匹配的 catch 模块,catch可能是同一函数中的一个局部的 catch,也可能是调用链中另一个函数的catch,控制权从throw位置转移到了catch位置。这里还有两个重要的含义:1. 沿着调用链的函数可能提早结束退出。2. 一旦程序开始执行异常处理,沿着调用链创建的对象都将销毁。
  • 抛出异常对象后,会生成一个异常对象的1拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝的对象会在catch子句后销毁。( 这里的处理类似于函数的传值返回)

1.2 基础语法格式和最简示例

基本语法格式

/*----------------------------------------------------------------- try { // 可能抛出异常的代码 可能出错的函数(); } catch (异常类型1& e) { // 处理类型1异常 } catch (异常类型2& e) { // 处理类型2异常 } catch (...) { // 捕获任意类型异常(兜底处理) } -----------------------------------------------------------------*/

最简示例(除零异常):

#include<exception>doubleDivide(int a,int b){// 当 b == 0 时抛出异常if(b ==0){//string s("Divide by zero condition!");//throw s;throwexception("Divide by zero condition!");}else{return((double)a /(double)b);}}voidFunc(){try{int len, time; cin >> len >> time; cout <<Divide(len, time)<< endl;}catch(const exception& e){ cout << e.what()<< endl;} cout <<"Func():"<<__LINE__<< endl;}intmain(){while(1){try{Func();}// 异常会先匹配最适配的catch(const string& s){ cout << s << endl;}catch(const exception& e){ cout << e.what()<< endl;}catch(...)// 任意类型的对象{ cout <<"未知异常"<< endl;} cout <<"Func():"<<__LINE__<< endl;}return0;}

二. 异常的核心机制:栈展开与匹配规则

2.1 栈展开

抛出异常后,程序会暂停当前函数执行,沿调用链向上查找匹配的catch块,这个过程称为 “栈展开”:

  1. 检查当前函数的try/catch块,若找到匹配的catch,则执行处理逻辑;
  2. 若未找到,销毁当前函数的局部对象,退出当前函数,继续向上查找;
  3. 重复步骤 1-2,直到找到匹配的catch
  4. 若到达main函数仍未找到,调用terminate函数终止程序。

补充

在这里插入图片描述
在这里插入图片描述


栈展开示例:

voidFunc1(){throw"Func1抛出异常";// 抛出异常}voidFunc2(){Func1();// 调用Func1,不处理异常}voidFunc3(){Func2();// 调用Func2,不处理异常}intmain(){try{Func3();// 调用Func3}catch(constchar* errmsg){// 捕获Func1抛出的异常(栈展开:Func1→Func2→Func3→main) cout <<"捕获异常:"<< errmsg << endl;}return0;}

2.2 异常捕获的匹配规则

捕获异常时,遵循 “精确匹配优先、兼容转换次之” 的原则:

  • 优先匹配与抛出对象类型完全一致的catch
  • 支持有限的类型转换:
    • 非常量→常量(intconst int);
    • 数组→数组元素指针(int[5]int*);
    • 派生类→基类(最实用,用于自定义异常体系);
  • 若有多个catch块,按顺序匹配,匹配成功后不再检查后续catch
  • catch (...)可捕获任意类型异常,通常作为兜底,避免程序终止。

补充

在这里插入图片描述

三. 自定义异常体系:大型项目的最佳实践

在大型项目中,直接抛出基本类型(如字符串、整数)的异常难以区分错误类型,推荐自定义异常类体系(基于继承),统一异常接口,便于管理和扩展。

3.1 自定义异常体系设计 && 异常抛出与捕获实战

核心思路:定义一个基类Exception,派生类对应不同模块的异常(如 SQL 异常、缓存异常、HTTP 异常),通过多态返回详细错误信息。

代码实现:

#include<thread>// 一般大型项目程序才会使用异常,下面我们模拟设计一个服务的几个模块// 每个模块的继承都是Expection的派生类,每个模块可以添加自己的数据// 最后捕获的时候,我们捕获基类就可以,通过多态可以打印不同信息classException{public:Exception(const string& errmsg,int id):_errmsg(errmsg),_id(id){}virtual string what()const{return _errmsg;}intgetid()const{return _id;}protected: string _errmsg;int _id;};classSqlException:publicException{public:SqlException(const string& errmsg,int id,const string& sql):Exception(errmsg,id),_sql(sql){}virtual string what()const{ string str ="SqlException:"; str += _errmsg; str +="->"; str += _sql;return str;}private:const string _sql;};classCacheException:publicException{public:CacheException(const string& errmsg,int id):Exception(errmsg, id){}virtual string what()const{ string str ="CacheException:"; str += _errmsg;return str;}};classHttpException:publicException{public:HttpException(const string& errmsg,int id,const string& type):Exception(errmsg, id),_type(type){}virtual string what()const{ string str ="HttpException:"; str += _type; str +=":"; str += _errmsg;return str;}private:const string _type;};voidSQLMgr(){if(rand()%7==0){throwSqlException("权限不足",100,"select * from name = '张三'");}else{ cout <<"SQLMgr 调用成功"<< endl;}}voidCacheMgr(){if(rand()%5==0){throwCacheException("权限不足",100);}elseif(rand()%6==0){throwCacheException("数据不存在",101);}else{ cout <<"CacheMgr 调用成功"<< endl;}SQLMgr();}voidHttpServer(){if(rand()%3==0){throwHttpException("请求资源不存在",100,"get");}elseif(rand()%4==0){throwHttpException("权限不足",101,"post");}else{ cout <<"HttpServer调用成功"<< endl;}CacheMgr();}intmain(){srand(time(0));while(1){ this_thread::sleep_for(chrono::seconds(1));try{HttpServer();}catch(const Exception& e)// 这里捕获基类,基类对象和派生类对象都可以被捕获{// 多态调用 cout << e.what()<< endl;}catch(...){ cout <<"Unkown Exception"<< endl;}}return0;}

部分输出演示

在这里插入图片描述

四. 异常的高级用法

4.1 异常重新抛出

有时捕获异常后,无法完全处理(如仅记录日志),或需要根据错误类型分流处理,可通过throw;重新抛出异常,让外层调用链继续处理。

示例:网络请求重试

// 下面程序模拟展示了聊天时发送消息,发送失败补货异常,但是可能在// 电梯地下室等场景手机信号不好,则需要多次尝试// 如果多次尝试都发送不出去,则就需要捕获异常再重新抛出,// 其次如果不是网络差导致的错误,捕获后也要重新抛出。void_SendMsg(const string& s){if(rand()%2==0){throwHttpException("网络不稳定,发送失败",102,"put");}elseif(rand()%7==0){throwHttpException("你已经不是对方的好友,发送失败",102,"put");}else{ cout <<"发送成功"<< endl;}}// 网络不稳定,要求重试三次,均失败voidSendMsg(const string& s){for(size_t i =0; i <4; i++){try{_SendMsg(s);// 走到这里,如果没有抛异常导致结束// 那就代表成功了,可以执行到这个break,跳出循环break;}catch(const Exception& e){if(e.getid()==102){if(i ==3)throw; cout <<"开始第"<< i +1<<"重试"<< endl;}else{// 重新抛出异常// throw e;throw;}}}}intmain(){srand(time(0)); string str;while(cin >> str){try{SendMsg(str);}catch(const Exception& e){ cout << e.what()<< endl << endl;}catch(...){ cout <<"Unkown Exception"<< endl;}}return0;}

4.2 异常安全:避免资源泄漏

  • 异常抛出后,当前函数后续代码不再执行,若之前申请了资源(内存、锁、文件句柄),未及时释放会导致资源泄漏,这是异常使用的核心痛点。

解决方案:

  • 手动捕获释放:在catch中释放资源后重新抛出异常;
  • RAII 机制:利用类的构造 / 析构自动管理资源(推荐,如智能指针、自定义资源管理类)后面的博客中还会再详细讲的;
  • 析构函数不抛异常:析构函数若抛出异常,可能导致资源释放不完全,需在析构函数内部捕获处理。

示例:

doubleDivide(int a,int b){// 当b == 0时抛出异常if(b ==0){throw"Division by zero condition!";}return(double)a /(double)b;}voidFunc(){// 这里可以看到如果发生除0错误抛出异常,那下面的array就没有得到释放。// 所以这里捕获异常后并不处理异常,// 异常还是交给外层处理,这里捕获了再重新抛出去。int* array =newint[10];int len, time; cin >> len >> time;try{ cout <<Divide(len, time)<< endl;}catch(...){ cout <<"delete []"<< array << endl;delete[] array;// 重新抛出,捕获到什么抛出什么throw;} cout <<"delete []"<< array << endl;delete[] array;}intmain(){try{Func();}catch(constchar* errmsg){ cout << errmsg << endl;}catch(...){ cout <<"Unkown Exception"<< endl;}return0;}

4.3 异常规范( noexcept )

C++11 提供noexcept关键字,用于声明函数是否会抛出异常,帮助编译器优化代码:

  • 函数声明 noexcept:表示函数不会抛出异常;
  • 函数声明 noexcept(表达式):表达式为true时,证明该函数不抛异常(主要是用来确认和验证);
  • 若声明noexcept的函数实际抛出异常,程序会调用terminate终止(根本没有机会捕获)

补充

在这里插入图片描述


实际示例

// C++11标记不会抛异常的方法// double Divide(int a, int b) noexcept// C++98用来标记会抛异常的方法// double Divide(int a,int b) throw(const char*)// C++98// 这里表示这个函数只会抛出bad_alloc的异常// void* operator new (std::size_t size) throw (std::bad_alloc);// 这里表示这个函数不会抛出异常// void* operator delete (std::size_t size, void* ptr) throw();// C++11// size_type size() const noexcept;// iterator begin() noexcept;// const_iterator begin() const noexcept;doubleDivide(int a,int b)noexcept{// 当b == 0时抛出异常if(b ==0){throw"Division by zero condition!";}return(double)a /(double)b;}intmain(){try{int len, time; cin >> len >> time; cout <<Divide(len, time)<< endl;}catch(constchar* errmsg){ cout << errmsg << endl;}catch(...){ cout <<"Unkown Exception"<< endl;}int i =0; cout <<noexcept(Divide(1,2))<< endl; cout <<noexcept(Divide(1,0))<< endl; cout <<noexcept(++i)<< endl;return0;}

五. C++ 标准库异常体系

C++ 标准库提供了一套预定义的异常继承体系,基类为std::exception,派生类对应不同类型的标准异常(如内存分配失败、数组越界),可直接使用或继承扩展。

在这里插入图片描述

标准库异常体系核心类:

异常类用途错误信息获取方式
std::exception所有标准异常的基类what()(虚函数)
std::bad_allocnew分配内存失败时抛出what()返回 “bad alloc”
std::out_of_range数组/容器越界时抛出what()返回越界信息
std::invalid_argument无效参数时抛出what()返回参数错误信息
  • 不过我们日常的话一般使用 std::exception 就OK了

结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 

结语:C++ 异常机制是大型项目错误处理的首选方案,它让错误处理逻辑与业务逻辑分离,代码更清晰、可维护。掌握异常的基本语法、栈展开机制、自定义异常体系和异常安全,能让你在应对复杂错误场景时游刃有余。实际开发中,建议结合 RAII 机制(如智能指针)解决资源泄漏问题,基于标准库std::exception扩展自定义异常,让异常处理既优雅又安全。

✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど

Read more

初阶数据结构之栈的实现

初阶数据结构之栈的实现

前言:实现栈之前,先来了解一下什么是栈。 1. 栈的概念 栈是一种特殊的线性表,只允许在固定一端插入和删除操作,进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守先进后出,后进先出LIFO(Last In First Out)的原则。 压栈:栈的插入操作叫做进栈(压栈,入栈),入数据在栈顶。 出栈:栈的删除操作叫做出栈,出数据也在栈顶。 2. 栈的底层结构如何选择 现在我们已经了解了栈的结构特性了。那么我们该如何实现栈呢?先来看一个问题,顺序表和链表我们已经学习过了,那么栈的底层结构应该选择哪一个呢? 如果底层结构采用数组实现,在插入元素时只需要在指定的位置插入元素即可,删除元素时,- -top就可以了。唯一的缺点就是会存在空间浪费。 如果采用链表来实现栈,每一次插入数据元素都要开辟空间并且需要遍历链表,使新节点成为链表的尾节点;删除数据元素时也需要遍历链表,将尾节点的空间还给操作系统,还要保证尾节点的前驱节点的next保存NULL,避免成为野指针。优点是按需申请和释放空间,不存在空间浪费。缺点是时间复杂度为O(N),空间复杂度也为O(N)

By Ne0inhk
【优选算法】滑动窗口算法:专题一

【优选算法】滑动窗口算法:专题一

目录 引言:  【209. 长度最小的子数组】 题目描述: 实现核心及思路: 思路可视化: 代码实现: 【无重复字符的最长子串】 题目描述: 实现核心及思路: 思路可视化: 代码实现: 【最大连续1的个数III】 题目描述: 实现核心及思路: 代码实现: 【1658.将x减到0的最小操作数】 题目描述: 实现核心即思路: 代码实现: 引言: 滑动窗口?用两个指针维护一个动态的 “窗口” 区间,通过移动指针来扩大或缩小窗口,在一次遍历中完成计算,时间复杂度通常为 O (n)。 典型应用:寻找最长无重复字符的子串找到和为目标值的最短子数组字符串的排列匹配 一般步骤(模板): (1)定义left 和 right 指针同时指向数组首元素; (2)当符合要求时,right++,模拟进窗口; (3)不满足要求时,left++,模拟出窗口; (4)

By Ne0inhk
《算法闯关指南:优选算法--前缀和》--29.和为k的子数组,30.和可被k整除的子数组

《算法闯关指南:优选算法--前缀和》--29.和为k的子数组,30.和可被k整除的子数组

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 29. 和为k的子数组 * 解法(前缀和+哈希表): * 算法思路: * C++算法代码: * 算法总结&&笔记展示: * 30. 和可被k整除的子数组 * 解法(前缀和+哈希表): * 前置知识补充: * 算法思路: * C++算法代码: * 算法总结&&笔记展示: * 结尾: 前言: 聚焦算法题实战,系统讲解三大核心板块:优选算法:剖析动态规划、二分法等高效策略,学会寻找“最优解”。 递归与回溯:掌握问题分解与状态回退,攻克组合、

By Ne0inhk
【Linux系统】解明进程优先级与切换调度O(1)算法

【Linux系统】解明进程优先级与切换调度O(1)算法

各位读者大佬好,我是落羽!一个坚持不断学习进步的学生。 如果您觉得我的文章还不错,欢迎多多互三分享交流,一起学习进步! 也欢迎关注我的blog主页:落羽的落羽 文章目录 * 一、进程优先级的概念 * 二、查看优先级信息 * 1. PRI 与 NI 的理解 * 2. 修改nice值 * 三、进程调度切换 * 1. list_head 与 prio_array 结构 * 2. 活跃140队列与过期140队列 * 四、补充概念:竞争、独立、并行、并发 一、进程优先级的概念 CPU的资源是有限的,所以CPU的运行队列中的所有进程是不可能同时得到资源的。这就是为什么运行队列是一个“队列”,而CPU分配资源的先后顺序,就是指进程的优先级。 二、查看优先级信息 使用ps -l命令,可以查看系统中更详细的进程信息:

By Ne0inhk