C++异常

ops/2025/3/18 8:55:22/

文章目录

  • 异常的概念
  • 异常的抛出(throw)和捕获(catch)
  • “栈展开”
  • 查找匹配的处理代码
  • 异常的重新抛出
  • 异常安全问题
  • 异常规范
  • 标准库的异常

异常的概念

异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后将解决问题的任务传递给程序的另一部分,检测环节无须知道问题的处理模块的所有细节

异常是众多面向对象的语言的一种解决错误的方式,它对比的是c语言处理错误的方式,c语言处理错误的方式一般是返回错误码,错误码的本质就是对错误信息的一种编号。对C++来说异常就比如,当new空间new失败时,就会抛异常,它返回的不是错误码,返回的是一个对象,这个对象包含错误相关的一些信息。还有就是,c语言在出现错误时,是调用到错误的当前那一层处理,也就是外层处理错误。对异常来说,在出现错误时,可以跳跃几层去处理问题。可以理解为出现问题和解决问题的位置离得很远

异常的抛出(throw)和捕获(catch)

  • 程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前的调用链决定了应该由哪一个catch的处理代码来处理异常

比如:当main函数调用f1,f1函数调用f2,f2函数调用f3时,若f3抛出了一个异常,不一定是由调用它的f2函数来处理,要根据抛出的对象的类型及调用链决定

  • 被选中的catch是调用链中与抛出对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误

代码举例:

#include <iostream>
#include<string>
using namespace std;double Divide(int a, int b)
{try{//b == 0时抛出异常if (b == 0){string s("Divide by zero condition!");throw s;}else{return ((double)a / (double)b);}}catch(int errid) //捕获的对象的类型是int{cout << "errid" << endl;}return 0;
}void func()
{int len, time;cin >> len >> time;try{cout << Divide(len, time) << endl;}catch (const char* errmsg)//捕获对象的类型是char*{cout << errmsg << endl;}cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;//若抛出的异常与该函数内部的catch匹配,则会往下走该条指令,或没有异常try之后也会走该条指令
}int main()
{while (1){try{func();}catch (const string& errmsg){cout << errmsg << endl;}}return 0;
}

上面的代码分析,当调用func函数,输入值,然后调用Divide函数,若b为0时,就会抛出一个异常,上述代码中抛出是一个string对象,那由哪个catch来捕获呢,是Divide函数中的catch来捕获吗,不是的,根据C++的捕获规则,是由与抛出对象类型匹配以及是离抛出异常位置最近的catch来捕获,那么上面的代码中,符合规则的只有main函数中的catch。所以当抛出异常时,编译器就会先在当前层寻找匹配的catch,没有匹配就再往上一层,找到了就会从找到的catch位置开始继续往下执行,若找到main函数了还没有找到,就会终止程序
在这里插入图片描述
注:这里的“寻找”只是形象的说法,其实在编译时就已经确定好了

  • 当throw执行时,throw后面的语句将不再被执行。程序的执行从throw位置跳到与之匹配的catch模块,catch可能是同一层函数中的一个局部的catch,也可能是调用链中另一个函数的catch,控制权从throw位置转移到了catch位置。还有两个重要的含义:1.当抛出异常时,沿着调用链的函数可能提早退出。2.一旦程序开始执行异常处理程序,沿着调用链创建出来的对象都将被销毁,当然这里的对象都会调用对应的析构来销毁
  • 抛出异常对象后,会生成异常对象的拷贝,因为抛出异常的对象可能是一个局部对象,对于局部对象它的生命周期是不会变的,所以会生成一个拷贝对象,这个拷贝对象会在catch之后销毁(类似于函数的传值返回)

“栈展开”

  • 抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理
  • 如果当前函数中没有try/catch子句,或者有但类型不匹配,则退出当前函数,继续在外层调用函数链中寻找,上述寻找catch的过程被称为栈展开
  • 若到达main函数,依旧没有匹配的catch子句,程序会调用标准的terminate函数终止程序,说明抛出了异常,一定是要被捕获的
  • 如果找到匹配的catch子句处理后,catch子句后的代码会继续执行

查找匹配的处理代码

  • 若到main函数,异常仍旧没有被捕获,就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般函数中最后都会使用catch(…),它可以捕获任意类型的异常,但是不知道异常的错误是什么

若将上面举例代码的main函数改为下面这样:

int main()
{while (1){try{func();}catch (const char* errmsg) //类型改成const char*{cout << errmsg << endl;}catch (...){cout << "未知异常" << endl;}}return 0;
}

在这里插入图片描述

将main函数改成上述代码后,抛出的string类型的对象,最后只能用catch(…)来进行捕获,虽然用来语句来捕获异常,我们不知道哪里出错,但它保证了程序不会终止,也告诉了我们有人乱抛异常

  • 一般情况下抛出对象和catch是类型完全匹配的,若有多个类型匹配的catch,就选择最近的那个,但已有一些例外,允许非常量向常量的类型转换,也就是权限缩小,抛出string对象,用const string来捕获;允许数组转换称指向数组元素类型的指针,函数被转成指向函数的指针;允许从派生类向基类的转换,这个点非常实用,实际中继承体系基本都是用这个方法设计

允许从派生类向基类的转换,说明在继承体系中,抛出了一个派生类对象,可以用一个基类对象来接受。这样的设计,确保了在继承体系中,只要抛出的是派生类对象,我都可以用基类对象来进行捕获,若发现抛出的异常没有被我的基类对象捕获,那就说明有人没有按规则来抛出异常

来看下面的代码举例:

#include<thread>
class Exception //基类
{
public://当抛出异常时,捕获错误信息和编号Exception(const string& errmsg,int errid):_errmsg(errmsg),_errid(errid){ }virtual string what()const {return _errmsg;}int Getid()const{return _errid;}
protected:string _errmsg;int _errid;
};//不同的模块有不同的个性需求,就比如下面的派生类需要再捕获到一些其他错误的信息
class SqlException : public Exception 
{
public:SqlException(const string& errmsg,int errid,const string& Sql):Exception(errmsg,errid),_Sql(Sql){ }virtual string what()const{string str = "SqlException:";str += _errmsg;str += "->";str += _Sql;return str;}
private:const string _Sql;
};class CacheExcepiton : public Exception
{
public:CacheExcepiton(const string& errmsg,int id):Exception(errmsg,id){ }virtual string what(){string str = "CacheExcepiton:";str += _errmsg;return str;}
};class HttpException : public Exception
{
public:HttpException(const string& errmsg, int id, const string& type):Exception(errmsg, id), _type(type){ }virtual string what(){string str = "HttpExcepion:";str += _type;str += ':';str += _errmsg;return str;}
protected:string _type;
};void SqlMgr()
{if (rand() % 7 == 0){//若随机值是5就抛出SQL的权限异常,并附上异常编号throw SqlException("权限不足", 101, "selecte * from name = 张三");}else{cout << "SqlMgr 调试成功" << endl;}
}void CacheMgr()
{if (rand() % 5 == 0){//若随机值是5就抛出cache的权限异常,并附上异常编号throw CacheExcepiton("权限不足", 101);}else if (rand() % 6 == 0){//若随机值是6就抛出cache的资源异常,并附上异常编号throw CacheExcepiton("资源不存在", 100);}else{cout << "CacheMgr 调试成功" << endl;}SqlMgr();
}void HttpServer()
{if (rand() % 3 == 0){//若随机值是3就抛出HTTP的资源异常throw HttpException("资源不存在", 100, "get");}else if (rand() % 4 == 0) {//若是4就抛出HTTP的权限异常throw HttpException("权限不足", 101, "post");}else{//其他值就说明没有异常,正常走代码cout << "HttpServer 调试成功" << endl;}CacheMgr();
}
int main()
{srand((unsigned int)time(0));while (1){this_thread::sleep_for(chrono::seconds(1));try{HttpServer();}catch (const Exception& e)//抛派生类,用基类捕获{//多态的调用cout << e.what() << endl;}catch(...){cout << "Unknow Exception" << endl;}}return 0;
}

上述代码,实现了一个模拟程序,用随机值来进行模拟,因为现实当中,出现异常是偶然出现的。main函数中只捕基类和未知异常,代码中运用了经典的多态,基类就是捕获基本的异常,派生类则是在此基础上,根据自己的需求对基类进行重写。

运行的部分结果如下:

在这里插入图片描述
若没有抛基类或者派生类,就会用catch(…)来捕获

异常的重新抛出

有时catch捕获到一个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接throw,就可以把捕获的对象直接抛出。

举例:下面的代码是一个简单的线上聊天软件的项目

#include<iostream>
#include<string>
using namespace std;class Exception
{
public:Exception(const string& errmsg,int errid):_errmsg(errmsg),_errid(errid){ }virtual string what()const{return _errmsg;}int Getid()const{return _errid;}
protected:string _errmsg;int _errid;
};
class HttpException : public Exception
{
public:HttpException(const string& errmsg, int id, const string& type):Exception(errmsg, id), _type(type){ }virtual string what(){string str = "HttpExcepion:";str += _type;str += ':';str += _errmsg;return str;}
protected:string _type;
};
void _sendmsg(const string& s)
{if (rand() % 2 == 0){throw HttpException("网络不稳定,发送失败", 102, "put");}else if (rand() % 7 == 0){throw HttpException("你已不是对方的好友,发送失败", 103, "put");}else{cout << "发送成功" << endl;}
}
void sendmsg(const string& s)
{//发送消息失败,则再重新发送三次for (size_t i = 0; i < 4; i++){try{_sendmsg(s);break;}catch(const Exception& e){//捕获异常,若错误编号是102,则网络不稳定重新发送//若错误编号不是102,则不是对方好友,异常重新抛出if (e.Getid() == 102){//当i==3时,说明重新发送了三次还是失败,说明网络太差了,重新抛出异常if (i == 3){throw;//异常重新抛出}cout << "开始第" << i + 1 << "重试" << endl;}else{throw;//直接抛出异常}}}
}int main()
{srand((unsigned int)time(0));string str;while (cin >> str)//输入发送的信息{try{sendmsg(str);}catch (const Exception& e){cout << e.what() << endl << endl;}catch (...){cout << "Unknow Exception" << endl;}}return 0;
}

代码解析:在聊天时,消息发送不出去有很多种原因,可能是网络太差,又或者是对方已把你删除等等,上面的代码就是模拟对这两种原因进行处理。当需要发送信息时,如果网络正常且是对方好友,那么就可以正常发送,若是对方好友,但是网络太差,则会重新抛出一个异常提示网络不稳定,发送失败,但是我们知道当网络不稳定时,发送消息的文字前面是会有个灰色的圈圈在转的,那就说明消息还在尝试发送出去中,所以上面的代码也模拟实现了当第一次抛出网络不稳定的异常后,会被捕获,继续重新发送三次,若三次都发送失败,则会将异常重新抛出若不是对方好友,那么异常将会直接抛出

运行部分结果如下:
在这里插入图片描述
重新抛出异常,有时候不是为了对异常进行处理,可能是先捕获下来,对一些开辟的空间进行释放,或者特殊的情况先进行处理,处理完之后再重新抛出异常

异常安全问题

  • 我们知道当异常抛出后,后面的代码就不再执行,当我们申请了资源时,那么就会导致没有释放掉,这里就由于异常的原因导致内存泄漏,产生安全性的问题。这里我们就需要先将异常捕获下来,释放资源,再将异常重新抛出
  • 其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数需要释放10个资源,释放到第五个时需要抛出异常,则也需要捕获处理,不然就会内存泄漏

异常规范

对于用户和编译器而言,要是提前预知某个函数会不会抛异常,会有很多的好处,有助于简化调用函数的代码

  • 函数列表后面加上noexcept表示不会抛出异常,什么都不加表示可能会抛出异常,若一个声明了noexcept的函数抛出了异常,程序就会调用terminate终止程序
  • noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回false,不会则返回true

标准库的异常

C++标准库中也有一套自己的异常继承体系库,基类是exception,所以我们日常写程序,在主函数中,捕获exception即可,要获取异常信息,调用what函数,what是一个虚函数,派生类可以对其进行重写

在这里插入图片描述

对于异常的总结,就是捕获基类抛派生类


http://www.ppmy.cn/ops/166724.html

相关文章

Linux查找tomcat的路径

在 Linux 系统中&#xff0c;查找 Tomcat 的安装路径可以通过以下几种方法实现&#xff1a; 方法 1&#xff1a;通过 ps 命令查找 运行以下命令&#xff0c;查找 Tomcat 的进程&#xff1a; ps -ef | grep tomcat输出结果中会显示 Tomcat 的启动命令&#xff0c;例如&#xff1…

C++抽象与类的核心概念解析

在C中&#xff0c;抽象&#xff08;Abstraction&#xff09; 是面向对象编程&#xff08;OOP&#xff09;的核心概念之一&#xff0c;它通过隐藏复杂的实现细节&#xff0c;仅暴露必要的接口来实现对现实世界的简化建模。类&#xff08;Class&#xff09; 是实现抽象的核心工具…

企业管理杂谈:产品经理的选拔和培养——企业产品创新发展的关键

企业管理杂谈&#xff1a;产品经理的选拔和培养——企业产品创新发展的关键 恒易管理咨询 产品经理才是掌握未来企业产品发展方向的掌舵人。今天就想就“产品经理”这个概念&#xff0c;继续深入探讨一下企业如何选拔和培养什么样的产品经理。 一、产品经理的战略地位 在商业世…

XML 树结构

XML 树结构 引言 XML(可扩展标记语言)是一种用于存储和传输数据的标记语言,它以树状结构存储信息。XML 树结构是一种用于描述和表示XML文档的逻辑结构的方法。本文将深入探讨XML树结构的概念、特点及其在数据处理中的应用。 XML 树结构的基本概念 XML 文档结构 XML 文档…

时间语义与窗口操作:Flink 流式计算的核心逻辑

在实时数据流处理中&#xff0c;时间是最为关键的维度之一。Flink 通过灵活的时间语义和丰富的窗口类型&#xff0c;为开发者提供了强大的时间窗口分析能力。本文将深入解析 Flink 的时间语义机制&#xff0c;并通过实战案例演示如何利用窗口操作实现实时数据聚合。 一、Flink…

python语言写的一款pdf转word、word转pdf的免费工具

Word 与 PDF 文件转换工具 这是一个简单的 Web 应用程序&#xff0c;允许用户将 Word 文档转换为 PDF 文件&#xff0c;或将 PDF 文件转换为 Word 文档。 功能特点 - Word (.docx) 转换为 PDF - PDF 转换为 Word (.docx) - 简单易用的 Web 界面 - 即时转换和下载 - 详细的…

前端(vue)学习笔记(CLASS 4):组件组成部分与通信

1、组件的三大组成部分&#xff08;结构/样式/逻辑&#xff09; 注意点&#xff1a; 1、结构只能有一个根元素 2、全局样式&#xff08;默认&#xff09;&#xff0c;影响所有组件&#xff1b;局部样式&#xff0c;scoped下样式&#xff0c;只作用于当前组件 3、el根实例独…

移除元素(快慢指针)

原题链接&#xff1a;27.移除元素 由题意可知需要移除等于val的值&#xff0c;并且将不等于val的值顺序前移&#xff0c;但是返回顺序不重要 此时思考使用快慢指针即可 逆向思维一下 快指针val等于nums[fast]时自增&#xff0c;而不等于时&#xff0c;将快指针指向的值赋予慢指…