【C++】C++异常

【C++】C++异常



🎬 个人主页MSTcheng · ZEEKLOG
🌱 代码仓库MSTcheng · Gitee
🔥 精选专栏: 《C语言
数据结构
《算法学习》
C++由浅入深

💬座右铭:路虽远行则将至,事虽难做则必成!


在前面的文章中,我们已经介绍了C++11的一些新特性。本文将和下一篇一起为大家讲解C++的最后两个重要主题:异常处理和智能指针。

文章目录

一、异常的概念及使用

1.1异常的概念

异常(Exception)是指在程序执行过程中发生的意外或错误情况,这些情况可能导致程序无法继续正常执行。异常处理是编程中用于管理这些意外情况的机制,旨在提高程序的健壮性和用户体验。
相比于C语言,C语言主要通过错误码的方式处理错误,而错误码的本质就是对错误的信息进行分类编写。拿到错误码以后还要去查询错误信息,是比较麻烦的。而异常时直接抛出一个对象,这个对象可以涵盖更全面的错误信息。

1.2异常的分类

1、编译时异常Checked Exception

  • 这类异常在编译阶段就会被检查,必须显式处理(捕获或声明抛出)。
    常见于外部资源操作,如文件不存在(FileNotFoundException)、数据库连接 失败等。

2、运行时异常Runtime Exception

  • 编译时不会被强制检查,通常由逻辑错误引发,如空指针访问NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。

3、错误Error

  • 指严重问题(如内存耗尽OutOfMemoryError),通常无法通过程序处理,需从系统层面解决。

1.3异常的抛出与捕获

异常的抛出:

  • 当程序出现问题的时候,首先通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前调用链来决定匹配哪个catch,然后再被这个catch接收并处理异常。

异常的捕获:

  • 异常的捕获首先通过一个try/catch语句来捕获,并且该catch要与throw对象的类型匹配且为距离抛出异常位置最近的那一个catch,然后根据抛出的对象的类型和内容告知异常部分到底发生了什么错误。

下面我们就来看看C++的异常抛出与捕获机制:

#include<iostream>#include<string>usingnamespace std;//=================================//Divide这个函数是用来计算两个数相除的函数//对于一个除数来说被除数是不能为0的 也就是分子可以为0 但分母不能//所以我们针对被除数是否为0设计了一个异常机制//假设a为除数b为被除数 如果检测到b为0那么就抛出异常//=================================doubleDivide(int a,int b){try{// 当b == 0时抛出异常 引发除零错误if(b ==0){//===============================//抛出异常对象后,会⽣成⼀个异常对象的拷⻉,//因为抛出的异常对象可能是⼀个局部对象,//所以会⽣成⼀个拷⻉对象,//这个拷⻉的对象会在catch⼦句后销毁。//(这⾥的处理类似于函数的传值返回)//=============================== string s("Divide by zero condition!");throw s;//抛出的是一个string对象 catch的时候要用string类型接收}else{return((double)a /(double)b);}//... fxx()}catch(constint& s){ cout << s << endl;}//第一个catch 这个catch的类型与抛出异常的string对象类型匹配 且离抛出位置最近/*catch (const string& errmsg) { cout << errmsg << endl; }*/ cout << __FUNCTION__ <<":"<<__LINE__<<"行执行"<< endl;return0;}voidFunc(){int len, time; cin >> len >> time; cout <<Divide(len, time)<< endl; cout << __FUNCTION__ <<":"<<__LINE__<<"行执行"<< endl;}intmain(){try{Func();}//第二个catch 虽然也是string类型 与抛出异常的string对象类型配但是离抛出位置较远//所以第一个catch存在的情况下程序优先会跳到第一个catch中catch(const string& errmsg){ cout << errmsg << endl;}catch(int errid){ cout << errid << endl;}}
1、当被第一个catch捕获时

2、注释掉第一个catch,当被第二个catch捕获时

注意事项:

  1. throw执行时,throw后面的语句将不再执行,直接跳转到对应的catch中去执行catch可能是同一个函数中的局部catch,也可能是调用链中另一个函数中的catch,制空权从throw位置转移到了catch位置之后还有两个重要含义:
    1、沿着调用链的函数可能提早退出
    2、一旦程序开始执行异常处理程序,沿着调用链创建的对象都将销毁。
对于第一点含义我们对比两次catch执行的结果就会发现,当第一个catch捕获时,由于第一个catch与抛出异常的对象位于同一个域,所以当异常对象s被第一个catch捕获时并没有跳过func函数所以程序运行时会执行func函数中的内容而第二次catch就不同了,第二个catchmain函数中,throw之后直接跳过func函数,到main函数中执行catch之后的内容,跳过了func函数所以func函数中的内容不被执行。

对于第二点,我们就要来看看栈的展开了

1.4栈展开

如果一直到main函数都没有找到了与之类型匹配的catch子句,则程序会调用标准库的terminate函数终止程序,如下图:

在这里插入图片描述

当抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句首先检查throw本⾝是否在try块内部,如果在则查找匹配的catch语句:如果有匹配的,则跳到catch的地方进行处理;如果当前函数中没有try/catch子句,或者try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开。 下面来看看栈展开图

在这里插入图片描述

1.5 查找匹配的处理代码

⼀般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。
但是也有⼀些例外,允许从非常量向常量的类型转换,也就是权限缩小允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针允许从派⽣类向基类类型的转换,这个点非常实⽤,实际中继承体系基本都是用这个方式式设计的。下面来看一些例子:
//=============第一种==============//精确匹配try{throw42;// 抛出int类型}catch(int e){// 精确匹配int std::cout <<"Caught int: "<< e;}//=============第二种===============//权限缩小转换(非常量->常量)try{char* ptr =newchar[10];throw ptr;// 抛出char*}catch(constchar* e){// 允许非常量转常量 std::cout <<"Caught const pointer";delete[] e;}//=============第三种==============//派生类->基类转换classBase{virtualvoidfoo(){}};classDerived:publicBase{};try{throwDerived();// 抛出派生类对象}catch(Base& e){// 捕获基类引用(多态处理) std::cout <<"Caught Base reference";}//==============第四种=============//数组->指针转换try{int arr[5]{1,2,3};throw arr;// 抛出int[5]}catch(int* e){// 自动转为指针 std::cout <<"Caught pointer to first element: "<< e[0];}//==============第五种=============//就近原则匹配catchtry{throw std::string("error");}catch(const std::string& e){// 优先匹配更近的 std::cout <<"Caught by string ref";}catch(const std::exception& e){ std::cout <<"Caught by exception ref";}

另外:如果到main函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以⼀般main函数中最后都会使****⽤catch(...),它可以捕获任意类型的异常,但是是不知道异常错误是什么。

#include<iostream>#include<stdexcept>intmain(){try{// 可能抛出异常的代码throw std::runtime_error("An error occurred");}catch(const std::exception& e){ std::cerr <<"Caught exception: "<< e.what()<<std::endl;}catch(...)//使用三个点来接收异常{ std::cerr <<"Caught an unknown exception"<< std::endl;}return0;}

1.6异常重新抛出

有时catch到⼀个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出
try{// 可能抛出异常的代码someRiskyOperation();}catch(const std::exception& e){if(isSpecialError(e)){// 特殊错误处理handleSpecialCase();}else{// 其他错误重新抛出throw;// 注意没有参数}}

注意throwthrow e的区别:

  1. throw重新抛出当前异常对象,不进行拷贝构造。
  2. throw e会通过拷贝构造函数创建一个新的异常对象。

1.7异常的安全问题

由于异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。中间我们需要捕获异常,释放资源后⾯再重新抛出。当然后面智能指针章节讲的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];try{int len, time; cin >> len >> time; 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(const exception & e){ cout << e.what()<< endl;}catch(...){ cout <<"Unkown Exception"<< endl;}return0;}

1.8异常规范

相比于传统C++98时的异常规范C++11中进行了简化:函数参数列表后面加
noexcept表示不会抛出异常,什么都不加表示可能会抛出异常。
  • 编译器并不会在编译时检查noexcept,也就是说如果⼀个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是⼀个声明了noexcept的函数抛出了异常,程序会调用terminate函数来终止程序。
  • 另外noexcept还可以作为一个运算符去检测一个表达式是否会抛出异常,如果会抛出异常就返回false,如果不会就返回true
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++的异常处理机制是管理运行时错误的重要工具,它通过try、catchthrow三个关键字的配合使用,能够有效应对程序执行中的意外情况。这套机制不仅确保了程序的健壮性,还能实现错误的优雅处理。因此,在代码编写时应当重视异常处理的应用。

MSTcheng 始终坚持用直观图解 + 实战代码,把复杂技术拆解得明明白白! 👁️ 【关注】 看普通程序员如何用实用派思路搞定复杂需求 👍 【点赞】 给 “不搞虚的” 技术分享多份认可 🔖 【收藏】 把这些 “好用又好懂” 的干货技巧存进你的知识库 💬 【评论】 来唠唠 —— 你踩过最 “离谱” 的技术坑是啥? 🔄 【转发】把实用技术干货分享给身边有需要的程序员伙伴 技术从无唯一解,让我们一起用最接地气的方式,写出最扎实的代码! 🚀💻 
感谢能够看到这里的小伙伴,如果这篇文章有帮到您,还请给个三连!你们的持续支持是我更新最大的动力!谢谢!

Read more

【大数据存储与管理】分布式文件系统HDFS:03 HDFS的相关概念

【大数据存储与管理】分布式文件系统HDFS:03 HDFS的相关概念

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈大数据技术原理与应用 ⌋ ⌋ ⌋专栏系统介绍大数据的相关知识,分为大数据基础篇、大数据存储与管理篇、大数据处理与分析篇、大数据应用篇。内容包含大数据概述、大数据处理架构Hadoop、分布式文件系统HDFS、分布式数据库HBase、NoSQL数据库、云数据库、MapReduce、Hadoop再探讨、数据仓库Hive、Spark、流计算、Flink、图计算、数据可视化,以及大数据在互联网领域、生物医学领域的应用和大数据的其他应用。 【GitCode】专栏资源保存在我的GitCode仓库:https://gitcode.com/Morse_Chen/BigData_principle_application。 文章目录 * 一、块 * 二、名称节点和数据节点 * 三、第二名称节点 * 小结 本文介绍 HDFS 中的相关概念,包括块、名称节点和数据节点、第二名称节点。

By Ne0inhk
Flutter 组件 vnlunar 适配鸿蒙 HarmonyOS 实战:高精度农历算法,构建民俗文化日期与节气治理架构

Flutter 组件 vnlunar 适配鸿蒙 HarmonyOS 实战:高精度农历算法,构建民俗文化日期与节气治理架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 vnlunar 适配鸿蒙 HarmonyOS 实战:高精度农历算法,构建民俗文化日期与节气治理架构 前言 在鸿蒙(OpenHarmony)生态迈向全球化部署、涉及多语言本地化(L10n)及深层文化特性适配的背景下,如何实现准确的阴阳历(农历)转换、二十四节气计算及民俗节日提醒,已成为提升应用“人文温度”与本地化竞争力的核心要素。在鸿蒙设备这类强调分布式时间同步与低功耗常驻显示(AOD)的环境下,如果应用依然依赖简单的查表法或通过网络接口获取农历信息,由于由于闰月计算的复杂性或离线环境限制,极易由于由于计算偏移导致传统节日提醒的误报。 我们需要一种能够实现天文级算法推演、支持高精度节气定位且具备纯 Dart 离线运作能力的历法治理方案。 vnlunar 为 Flutter 开发者引入了标准化的阴阳历转换协议。它不仅支持对天干地支、生肖及闰月的精确解构,更针对东南亚等地区的历法细微差异提供了专项适配。在适配到鸿蒙 HarmonyOS 流程

By Ne0inhk
【数据结构与算法】链表超全分类!从结构入门到双向链表初始化实现

【数据结构与算法】链表超全分类!从结构入门到双向链表初始化实现

🔥小龙报:个人主页 🎬作者简介:C++研发,嵌入式,机器人等方向学习者 ❄️个人专栏:《C语言》《【初阶】数据结构与算法》 ✨ 永远相信美好的事情即将发生 文章目录 * 前言 * 一、链表的分类与说明 * 1.1 单向或者双向 * 1.2 带头或者不带头 * 1.3 循环或者不循环 * 二、双向链表 * 2.1 双向链表的定义 * 2.2 双向链表中哨兵位头节点的初始化 * 三、代码展现 * 3.1 List.h * 3.2 List.c * 3.3 test.c * 总结与每日励志 前言 链表是数据结构入门阶段的核心知识点,

By Ne0inhk
【线性表系列终篇】链表试炼:LeetCode Hot 100 经典题目实战解析

【线性表系列终篇】链表试炼:LeetCode Hot 100 经典题目实战解析

🏠个人主页:黎雁 🎬作者简介:C/C++/JAVA后端开发学习者 ❄️个人专栏:C语言、数据结构(C语言)、EasyX、游戏、规划、程序人生 ✨ 从来绝巘须孤往,万里同尘即玉京 文章目录 * 【线性表系列终篇】链表试炼:LeetCode Hot 100 经典题目实战解析 * 文章摘要 * 一、试炼前的准备:链表解题核心技巧回顾 * 二、试炼开始:经典题目实战解析 * 题目一:反转链表 (LeetCode 206) * 解法一:迭代(双指针) * 解法二:递归 * 题目二:环形链表 (LeetCode 141) * 解法:快慢指针(Floyd判圈算法) * 题目三:合并两个有序链表 (LeetCode 21)

By Ne0inhk