【异常 - 错误的更优解决方案】

news2024/9/28 21:19:16

目录:

  • 前言
  • 异常
    • (一) c语言原有的错误处理方式
    • (二) 异常的概念
    • (三)异常的使用
      • 1.异常的抛出与捕捉
      • 2.函数调用链中异常栈的展开原则
    • (四)5组测试及对应结论
      • 1.常规测试
      • 2.异常重新抛出
    • (五)异常安全
    • (六)异常规格化
    • (七)自定义异常体系
    • (八)c++标注库中的异常体系
  • 总结

前言

打怪升级:第90天

异常

(一) c语言原有的错误处理方式

  1. 终止程序,如assert,当遇到错误时会直接终止进程,比如访问QQ空间,由于某些原因QQ空间无法查看,如果使用assert,就会直接把你的QQ退掉,显然用户无法接受。
  2. 返回错误码errno,C语言很多库的接口函数都是通过errno返回错误码,缺点是错误信息需要程序员自己查看,并且错误信息并不全面。

实际中,大多数情况都使用错误码标记错误,少数极端错误才会使用assert。


(二) 异常的概念

异常是错误处理的一种方式,当函数遇到自己无法处理的错误时就可以抛异常,由函数自己或函数的直接、间接调用者来处理异常。
throw:抛出异常,当遇到特定错误时就通过throw关键字抛出异常对象。
catch:捕捉异常,在你想要处理特定异常的地方,使用catch关键字捕捉该异常。
try:try块中为可能抛出异常的代码,只有在try块中的异常才能被对应的catch捕捉。

关键字使用:

try
{
	if(condition)  
	{
		throw(...);
	}
	else
	{}
}
catch(exception_type1  e)
{}
catch(exception_type2  e)
{}
catch(...)
{}


(三)异常的使用

1.异常的抛出与捕捉

  1. 异常抛出的是对象,根据对象的类型来决定应该激活哪段catch处理代码;
  2. 抛出的对象可以是任何类型;
  3. 异常的捕捉会可以在当前函数调用链的任意位置进行;
  4. 被选择的处理代码是与该异常类型匹配且在该调用链上距离异常最近的一个;
  5. catch(…)可以捕捉任意类型的异常,只是无法获取异常信息
  6. 异常类型匹配有一个特例:派生类的异常可以使用父类类型进行捕捉,这个在实际中非常常用。

2.函数调用链中异常栈的展开原则

  1. 首先检查throw本身是否在try块中,
    1.1 如果是,再检查是否有匹配的catch,
    1.1.1如果有就执行匹配的catch处理代码;
    1.1.2如果没有就沿着函数调用链往回查找是否处于调用函数的try块中,重复上述操作;
    1.2如果没有就沿着调用函数链往回查找是否处于调用函数的try块中,重复上述操作;
  2. 如果到main函数的栈中都不在try块中,或没有找到匹配的catch就会终止进程。

上述沿着调用链查找catch子句的过程,称为栈展开

这里是引用


(四)5组测试及对应结论

1.常规测试

#include<iostream>

using namespace std;

int main() {

	while (1)
	{
		int a, b;
		cin >> a >> b;
		if (b == 0)
			throw("division by zero");
		else
			cout << "a / b = " << a / b << endl;
	}


	return 0;
}

这里是引用
结论1:抛出异常后,如果不进行捕捉,依然会终止整个进程。

#include<iostream>

using namespace std;

int main() {

	while (1)
	{
		try
		{
			int a, b;
			cin >> a >> b;
			if (b == 0)
				throw("division by zero");
			else
				cout << "a / b = " << a / b << endl;
		}
		catch (const char* s) // 捕捉 const char* 类型的异常
		{
			cout << s << endl;
		}
	}


	return 0;
}

这里是引用
结论2:对异常进行捕捉后,可以获取异常对象(此处为一个字符串)

#include<iostream>

using namespace std;

int main() {

	while (1)
	{
		try
		{
			int a, b;
			cin >> a >> b;
			if (b == 0)
				throw("division by zero");
			else
				cout << "a / b = " << a / b << endl;
		}
//		catch (const char* s) // 捕捉 const char* 类型的异常
		catch (int s) // 捕捉 int 类型的异常
		{
			cout << s << endl;
		}
	}


	return 0;
}

这里是引用
结论3:要捕捉到异常,需要有与之对应的捕捉类型。

#include<iostream>
#include<string>

using namespace std;

void Div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw("division by zero");
	else
		cout << "a / b = " << a / b << endl;
	
}

void Func()
{
	Div();
}

int main() {

	while (1)
	{
		try
		{
			Func();
		}
		catch (const char* s)
		{
			cout << s << endl;
		}
	}

	return 0;
}

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

结论4:异常并非只能在哪里抛出就在哪里捕捉,而是会沿着调用链查找第一个与之匹配的catch捕捉。
并且这里的跳转到catch并非层层出栈,而是直接跳转到main函数中的catch位置,并没有再进入func函数(有坑)。


void Func()
{
	int* a = new int[10];
	try
	{
		Div();
	}
	catch (...)
	{
		cout << "delete array" << endl;
		delete[] a;

		throw;
	}

	cout << "delete array" << endl;
	delete[] a;
}

2.异常重新抛出

这里是引用在这里插入图片描述
结论5:抛出异常后,执行流会直接跳转到最近的类型匹配的catch,由于跳过了func函数,所以导致了内存泄漏,
我们需要让执行流在func位置停留,以便释放堆区空间,但是由于我们并不是在func中对异常进行处理,所以要再次转发出去。


(五)异常安全

  1. 构造函数用于初始化对象,尽量不要在构造函数处抛异常,防止对象初始化不完全;
  2. 析构函数用于销毁对象,尽量不要在析构函数处抛异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等);
  3. 由于抛异常后会直接跳转到类型匹配的catch位置,会导致一些函数后续执行被打乱,比如在new 和 delete之间导致内存泄漏、
    在lock 和 unlock之间导致死锁,在open 和 close 之间导致系统内存减少等,C++经常使用RAII来解决以上问题。

请查看上方最后一个示例。


(六)异常规格化

异常规格化是为了方便函数使用者知道该函数会抛出那些异常,具体做法为在函数参数列表之后加上 throw(类型…),列出这个函数可能抛出的所有异常,throw() 表示该函数不会抛出异常
但是需要注意,该方式只是说明可能抛出的异常类型,编译器不会进行检查,因此是否使用以及使用的规范性完全看函数设计者。

#include<iostream>
using namespace std;

void Func() throw(const char*)
{
	int a = rand() % 6;

	if (a == 0)
		throw("zero");
	else if (a == 1)
		throw(a);
	else
		cout << a << endl;
}

int main()
{
	while (1)
	{
		try
		{
			Func();
		}
		catch (const char* s)
		{
			cout << "const char* s: " << s << endl;
		}
	}
	return 0;
}

在这里插入图片描述

在这里插入图片描述
如图所示,上方的异常规格化方式为c++98的设定,由于没有严格的检查,在使用者之间也没有形成统一的规范,因此很多使用者选择弃而不用,因此这个规格化方式形同虚设。

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

  1. c++11中对规格化做了简化,如果该函数不可能抛异常就在函数后加上 noexcept(等同于 throw() ),否则就什么也不写,由用户自己去查看,但是同之前一样,noexcept也未进行严格的限制,上方只是报错;
  2. noexcept只会检查当前函数是否抛异常,嵌套一层就无法检查出;
  3. 设置了noexcept后,该函数即使抛异常,也无法被捕获,直接终止进程。

(七)自定义异常体系

上方我们所抛出的异常都是一个整形或者一个字符串,而这些提供的信息还是很少,并没有很大程度上改善报错信息的情况,
因此在实际中,我们一般会采用返回结构体对象的方式来获取更加详细的信息,此处就离不开继承与多态了。

在公司实际开发时,一个项目基本都是多个开发人员联合开发,如果对异常操作没有规定,程序猿A在异常抛出位置就进行捕捉,程序猿B则打算在main函数处进行统一处理,而程序猿C则是在有的处理了有的没有处理,
并且假设每只程序猿至少抛出了5种类型的异常,那么最后一位负责main函数处理的程序猿M怎么做,我是应该处理多少个异常,你这个异常捕捉后处理了没有,“哎,我这里怎么出错了没有抛异常”,“哎,你这个人捕捉了异常怎么不处理”。。。
因此为了统一异常的使用,公司一般会规定在main函数统一进行异常处理,并且使用多态来重写不同的错误情况。

下方我们模拟一个QQ用户登录并访问空间的场景,我们将异常打印在显示器上,而在公司中则是需要写入日志。

#include<iostream>
#include<Windows.h>

using namespace std;

class Exception
{
public:
	Exception(int erron, const string& msg)
		:_erron(erron)
		, _errmsg(msg)
	{}

	virtual void Print()
	{
		GetErr();
	}

	void GetErr()
	{
		cout << _erron << ": " << _errmsg << ", ";
	}

private:
	int _erron;    // 错误码
	string _errmsg; // 错误信息
};

class LoginID:public Exception  // 用户登录
{
public:
	LoginID(int erron, const string& msg, int id)
		:Exception(erron, msg)
		, _userid(id)
	{}

	virtual void Print() override
	{
		GetErr();
		cout << "_userid: " << _userid << endl;
	}

private:
	int _userid;  // 错误用户id
};

class Webio :public Exception  // 网络请求
{
public:
	Webio(int erron, const string& msg, int port)
		:Exception(erron, msg)
		, _portnum(port)
	{}

	virtual void Print() override
	{
		GetErr();
		cout << "_portnum: " << _portnum << endl;
	}

private:
	int _portnum;  // 错误端口号
};

class MySQL :public Exception  // 数据库服务
{
public:
	MySQL(int erron, const string& msg, int id)
		:Exception(erron, msg)
		, _userid(id)
	{}

	virtual void Print()override
	{
		GetErr();
		cout << "_userid: " << _userid << endl;
	}

private:
	int _userid;  // 错误用户id
};

void Func()
{
	int num = rand() % 10;
	if (num < 3)
	{
		throw(LoginID(1, "LoginID ERRON", num));
	}
	else if (num < 6)
	{
		throw(Webio(2, "Webio ERRON", num));
	}
	else if (num < 9)
	{
		throw(MySQL(3, "MySQL ERRON", num));
	}
	else
	{
		cout << "success, num = " << num << endl;
	}

}

int main()
{
	while (1)
	{
		try
		{
			Func();
		}
		catch (Exception& e)  // 多态
		{
			e.Print();
			Sleep(1000);
		}	
		catch (...)
		{
			cout << "unknow exception" << endl;
		}
	}
	return 0;
}

这里是引用


(八)c++标注库中的异常体系

exception

C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:
在这里插入图片描述在这里插入图片描述实际中我们可以可以去继承exception类实现自己的异常类。但是实际中很多公司像上面一样自己定义一套异常继承体系。因为C++标准库设计的不够好用。

new失败抛出异常:

// bad_alloc example
#include <iostream>     // std::cout
#include <new>          // std::bad_alloc
#include<exception>    // std::exception

using namespace std;

int main() {
	try
	{
		int* myarray = new int[int(1e10)];
	}
	/*catch (const exception& e)
	{
		cerr << "std::exception: " << e.what() << endl;
	}*/
	catch (bad_alloc& ba)  // 捕捉结果同上
	{
		cerr << "bad_alloc caught: " << ba.what() << endl;
	}
	return 0;
}

这里是引用


总结

  • C++异常的优点:
  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
  2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误;而异常则直接跳转到匹配的catch。
  3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
  4. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。
  • C++异常的缺点:
  1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
  2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
  3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
  4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
  5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func()noexcept;的方式规范化。

总结:异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外OO的语言基本都是用异常处理错误,这也可以看出这是大势所趋。



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1506670.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

牛客周赛 Round 36

赛况 C题可惜&#xff0c;比赛时模拟没有想明白&#xff0c;只对了一半&#xff0c;赛后看了大佬们的题解后恍然大悟&#xff0c;而F题是压根没思路&#xff0c;况且F题部分分也比较难拿。 题目列表 A-小红的数位删除 思路 将读入的数字整除10做三次后输出即可 参考代码 #inc…

车载诊断协议DoIP系列 —— AL IPv6地址分配通用DoIP报头结构

车载诊断协议DoIP系列 —— AL IPv6地址分配&通用DoIP报头结构 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师(Wechat:gongkenan2013)。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎么看自…

️ IP代理实操指南:如何在爬虫项目中避免封禁和限制 ️‍♂️

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

动静态库

inode inode用于管理文件属性和内容 一个文件只能有一个inode&#xff0c;一个inode可以对应多个文件名 Linux进程中&#xff0c;打开的每一个文件都有对应的文件inode属性和文件页缓冲区&#xff08;内存和磁盘的缓冲区&#xff09; 软硬链接 硬链接 多个文件指向同一个i…

2024年软件测试怎么自我提升?“我“该如何做?

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、掌握基本的测试…

【Python】实战教学:给定二维numpy数组,将其转为csv格式并保存(指定列名)

【Python】实战教学&#xff1a;给定二维numpy数组&#xff0c;将其转为csv格式并保存&#xff08;指定列名&#xff09; &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质…

利用YOLOv5模型进行锥桶识别

目录 1. YOLOv5模型简介 2. 准备数据集 3. 训练模型 4. 模型评估 5. 模型部署与应用 6. 注意事项 在计算机视觉领域&#xff0c;目标检测是一项重要的任务&#xff0c;它可以帮助我们识别图像或视频中的特定物体并进行定位。而YOLOv5是一种高效的目标检测模型&#xff0c…

某app zzReqSign 算法还原分析过程

文章目录 前言一、抓包确定目标二、反编译定位加密位置三、反编译定位加密位置四、frida hook验证五、进一步分析so六、算法还原结束语------------------------------------------------END-------------------------------------------- 前言 工欲善其事必先利其器 准备工具…

Java“树结构TreeNode”用法详解,二叉树用法实现代码!!!

一、TreeNode用法 在Java中&#xff0c;TreeNode通常用于表示树结构中的节点。在树结构中&#xff0c;每个节点可以有零个或多个子节点&#xff0c;而TreeNode就是这个树结构中的一个节点。通常&#xff0c;树结构是通过链式结构实现的&#xff0c;每个节点有指向其子节点的引…

ubuntu20.04环境搭建:etcd+patroni+pgbouncer+haproxy+keepalived的postgresql集群方案

搭建基于etcdpatronipgbouncerhaproxykeepalived的postgresql集群方案 宿主机操作系统:ubuntu20.04 使用kvm搭建虚拟环境(如没有安装kvm&#xff0c;请先自行安装kvm) 1、安装kvm服务 ①、查看虚拟支持 如果CPU 支持硬件虚拟化则输出结果大于0&#xff0c;安装kvm-ok命令检…

(黑马出品_06)SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式

&#xff08;黑马出品_06&#xff09;SpringCloudRabbitMQDockerRedis搜索分布式 微服务技术ES搜索和数据分析 今日目标1. 查询文档1.1.DSL查询分类1.2.全文检索查询1.2.1.使用场景1.2.2.基本语法1.2.3.示例 1.3.精准查询1.3.1.term查询1.3.2.ran…

CSP初赛备考—汉字与运算

汉字 英文字符 英文字符的编码有两种&#xff1a;①ASCII标准码&#xff0c;7位&#xff08;128个字符&#xff09;②ASCII扩展吗&#xff0c;8位&#xff08;256个字符&#xff09; 中文字符 汉字分为两级&#xff1a;①一级汉字&#xff1a;3755个&#xff0c;按汉语拼音字…

STM32CubeIDE基础学习-STM32CubeIDE软件程序下载方法

STM32CubeIDE基础学习-STM32CubeIDE软件代码下载方法 文章目录 STM32CubeIDE基础学习-STM32CubeIDE软件代码下载方法前言第1章 代码下载第2章 下载器固件更新总结 前言 编写完代码&#xff0c;一般都会选择在线下载程序的方式进行验证该程序是否正确&#xff0c;如果发现结果和…

不会用虚拟机装win10?超详细教程解决你安装中的所有问题!

前言&#xff1a;安装中有任何疑问&#xff0c;可以在评论区提问&#xff0c;博主身经百战会快速解答小伙伴们的疑问 BT、迅雷下载win10镜像&#xff08;首先要下载win10的镜像&#xff09;&#xff1a;ed2k://|file|cn_windows_10_business_editions_version_1903_updated_sep…

恋活2 仿原神人物卡系列2全合集打包

内含&#xff1a;炽沙话事人 芭别尔迪希雅镀金女团 -沙中净水镀金女团 -叶轮舞者珐露珊坎蒂丝柯莱可莉丽莎-叶隐芳名神里绫华-花时来信瑶瑶。 下载地址&#xff1a; https://www.changyouzuhao.cn/13661.html

HarBor私有镜像仓库安装部署

环境准备 #>>> redis $ yum -y install redis $ systemctl enable --now redis $ vim /etc/redis.conf modify: bind <ipaddress> $ systemctl restart redis#>>> nfs $ yum -y install nfs-utils $ mkdir -p /data/harbor $ vi /etc/exports /data/h…

最顶级的Unity团队都在使用的技巧!!!

作为该系列的第二篇文章&#xff0c;今天将给大家分享一下&#xff0c;Unity最资深的团队是如何设置物理、UI和音频的。希望可以帮助大家最大限度的使用Unity引擎。 第一篇给大家介绍了如何提高资源、项目配置和图形的性能&#xff0c;感兴趣的朋友千万不要错过了。 文章链接…

Autosar Crypto Driver学习笔记(一)

文章目录 Crypto DriverPre-ConfigurationCryptographic capabilities加密能力Available Keys可用密钥 General BehaviorNormal OperationFunctional RequirementsSynchronous Job ProcessingAsynchronous Job Processing Design NotesPriority-dependent Job Queue基于优先级的…

一元函数积分学——刷题(16

目录 1.题目&#xff1a;2.解题思路和步骤&#xff1a;3.总结&#xff1a;小结&#xff1a; 1.题目&#xff1a; 比较这两种题的求解方法 2.解题思路和步骤&#xff1a; 3.13&#xff1a; 这个题就很适合用万能公式&#xff0c;因为可以把1t2消掉&#xff1a; 也可以用三角…

大数据时代的数据保护:分布式存储系统的七大原则

第一原则&#xff1a;“灾”和“备”&#xff0c;区分容灾切换与数据备份的区别 管理对象 管理对象 防什么&#xff1f; 底层逻辑 核心评价指标 容灾切换 IT环境与业 物理灾难 …