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

目录:

  • 前言
  • 异常
    • (一) 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的语言基本都是用异常处理错误,这也可以看出这是大势所趋。



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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【布局:1688,阿里海外的新筹码?】1688重新布局跨境海外市场:第一步开放1688API数据采集接口

2023年底&#xff0c;阿里巴巴“古早”业务1688突然成为“重头戏”&#xff0c;尤其宣布正式布局跨境业务的消息&#xff0c;一度引发电商圈讨论。1688重新布局跨境海外市场&#xff1a;第一步开放1688API数据采集接口 2023年11月中旬&#xff0c;阿里财报分析师电话会上&…

保姆级讲解字符串函数(下篇)

目录 strtok的使用 strerror的使用 strstr的使用和函数模拟实现 strstr的使用 strstr函数模拟实现 接上篇&#xff1a;保姆级讲解字符串函数&#xff08;上篇&#xff09;&#xff0c;我们接着把剩下三个函数讲解完&#xff0c;继续跟着我的步伐一起学习呀. strtok的使用 …

2024蓝桥杯每日一题(多路归并)

一、第一题&#xff1a;鱼塘钓鱼 解题思路&#xff1a;多路归并优先队列 首先枚举能走到的距离然后再用优先队列将最大的值累加 【Python程序代码】 from heapq import * n int(input()) a [0] list(map(int,input().split())) b [0] list(map(int,input().spli…

解决ChatGPT发送消息没有反应

ChatGPT发消息没反应 今天照常使用ChatGPT来帮忙码代码&#xff0c;结果发现发出去的消息完全没有反应&#xff0c;即不给我处理&#xff0c;也没有抱任何的错误&#xff0c;按浏览器刷新&#xff0c;看起来很正常&#xff0c;可以查看历史对话&#xff0c;但是再次尝试还是一…

php集成修改数据库的字段

1.界面效果 2.代码 <?phpecho <form action"" method"post"><label for"table">表名:</label><input type"text" id"table" name"table"><br><div id"fieldsContaine…

在 .NET 项目中复制资源文件夹到生成目录

本文主要介绍在使用 Visual Studio 进行调试和发布时&#xff0c;如何在 .NET 项目中复制资源文件夹到生成目录。 1. 背景 在开发 .NET 项目的过程中&#xff0c;我们有时会遇到需要在 debug 、 release 或是发布时将资源文件夹复制到生成目录的需求。这些资源可能包括图片、配…

Mybaties-Plus saveBatch()、自定义批量插入、多线程批量插入性能测试和对比

一.背景 最近在做一个项目的时候&#xff0c;由于涉及到需要将一个系统的基础数据全量同步到另外一个系统中去&#xff0c;结果一看&#xff0c;基础数据有十几万条&#xff0c;作为小白的我&#xff0c;使用单元测试&#xff0c;写了一段代码&#xff0c;直接采用了MP(Mybati…

【Python编程基础6/6】双向选择的判断

目录 知识回顾 导入 if-else 执行顺序 特性 两种判断语句的对比 就近原则 空值 定义 非空 定义 在判断语句中的关系 应用场景 练习 Debug 总结 知识回顾 在上节课中&#xff0c;我们学习了 if 判断&#xff0c;如果布尔表达式成立&#xff0c;就执行后面的代码块…

数据结构(二)——线性表(顺序表)

二、线性表 2.1线性表的定义和基本操作 2.1.1 线性表的基本概念 线性表&#xff1a;是具有相同数据类型的 n 个数据元素的有限序列。(Eg:所有的整数按递增次序排列&#xff0c;不是顺序表&#xff0c;因为所有的整数是无限的)其中n为表长&#xff0c;当n0时线性表是一个空表…

kali当中不同的python版本切换(超简单)

kali当中本身就是自带两个python版本的 配置 update-alternatives --install /usr/bin/python python /usr/bin/python2 100 update-alternatives --install /usr/bin/python python /usr/bin/python3 150 切换版本 update-alternatives --config python 0 1 2编号选择一个即可…

人才推荐 | 高级半导体工艺工程师,美国凯斯西储大学电化学博士

编辑 / 木子 审核 / 朝阳 伟骅英才 伟骅英才致力于以大数据、区块链、AI人工智能等前沿技术打造开放的人力资本生态&#xff0c;用科技解决职业领域问题&#xff0c;提升行业数字化服务水平&#xff0c;提供创新型的产业与人才一体化服务的人力资源解决方案和示范平台&#x…

uniapp 云开发笔记

uniapp云开发官方文档https://uniapp.dcloud.io/uniCloud/learning.html 新建 关联云空间 云函数获取用户openID uniCloud API列表https://uniapp.dcloud.io/uniCloud/cf-functions.html#unicloud-api%E5%88%97%E8%A1%A8 自建云函数login event中包含前端传来的参数 uniCloud.…

LeetCode刷题笔记之两数相加【数组】【中等】

两数相加 刷题笔记 &#x1f565;日期&#xff1a; 2024/03/09 题目描述&#xff1a; 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同…

mysql 性能优化——磁盘刷脏页性能优化

前言 大家是不是感觉mysql 更新挺快的呀&#xff0c;有没有想过mysql 更新为什么那么快。按道理说&#xff0c;mysql 更新都是先找到这一行数据&#xff0c;然后在去更新。意味着&#xff0c;就有两次磁盘操作&#xff0c;一个是磁盘读&#xff0c;一个是磁盘写。如果真的是这…

使用 SPL 高效实现 Flink SLS Connector 下推

作者&#xff1a;潘伟龙&#xff08;豁朗&#xff09; 背景 日志服务 SLS 是云原生观测与分析平台&#xff0c;为 Log、Metric、Trace 等数据提供大规模、低成本、实时的平台化服务&#xff0c;基于日志服务的便捷的数据接入能力&#xff0c;可以将系统日志、业务日志等接入 …

【鸿蒙开发】第十七章 Web组件(一)

1 Web概述 Web组件用于在应用程序中显示Web页面内容&#xff0c;为开发者提供页面加载、页面交互、页面调试等能力。 页面加载&#xff1a;Web组件提供基础的前端页面加载的能力&#xff0c;包括&#xff1a;加载网络页面、本地页面、html格式文本数据。 页面交互&#xff1a…

Python学习之基础语法

一、HelloWorld 二、Python基础语法 2.1 字面量 定义&#xff1a;在代码中&#xff0c;被写下来的固定的值&#xff0c;称之为字面量。 常用的6种值的类型 字符串 Python中&#xff0c;字符串需要用双引号包围&#xff1b; 被双引号包围的都是字符串 666 13.14 "黑马…

YOLOv3: An Incremental Improvement

新网络是YOLOv2、Darknet-19中使用的网络和那些新奇的残余网络之间的混合方法。我们的网络使用连续的3 3和1 1卷积层&#xff0c;但现在也有一些快捷连接&#xff0c;并且明显更大。它有53个卷积层&#xff0c;所以我们叫它Darknet-53。 这个新网络比Darknet19强大得多&#…

misc40

下载附件&#xff0c;发现只有第三个wav文件需要密码&#xff0c;其他都可以看 打开 conversion.txt 二进制转十进制得到202013 开 一张普通的二维码.png&#xff0c;直接扫不出结果。 010查看图片尾部发现 Brainfuck 编码 解码得到&#xff1a; 和谐民主和谐文明和谐和谐和谐…

WebStorm 开启 eslint 自动格式化配置

之后在 ctrl s保存之后&#xff0c;webstorm 都会根据eslint 的规则自动格式化。