【C++从0到王者】第三十二站:异常

news2024/12/22 19:43:28

文章目录

  • 一、C语言传统的处理错误的方式
  • 二、C++异常概念
  • 三、异常的使用
  • 四、异常的抛出与捕获
    • 1.异常的抛出原则
    • 2.在函数调用链中异常栈展开匹配原则
  • 五、实际应用中的异常使用
  • 六、C++标准库的异常体系
  • 七、异常规范
  • 八、异常安全
  • 九、异常的优缺点
  • 总结

一、C语言传统的处理错误的方式

传统的错误处理机制:

  1. 终止程序,如assert,缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。
  2. 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到 errno 中,表示错误

实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。

二、C++异常概念

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或者间接的调用者处理这个错误。

  • throw:当问题出现时,程序会抛出一个异常。这是通过使用throw关键字来完成的
  • catch:在想要处理问题的地方,通过异常处理程序捕获异常。catch关键字用于捕获异常,可以有多个catch进行捕获。
  • try:try块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个catch块。

如果有一个块抛出一个异常,捕获异常的方法会使用try和catch关键字,try块中放置可能抛出异常的代码,try块中的代码被称为保护代码。

try
{
	// 保护的标识代码
}
catch (ExceptionName e1)
{
	// catch 块
}
catch (ExceptionName e2)
{
	// catch 块
}
catch (ExceptionName eN)
{
	// catch 块
}

三、异常的使用

我们先看一下下面这段代码

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
		throw "Division by zero condition!";
	else
		return ((double)a / (double)b);
}
void Func()
{
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	return 0;
}

在上面这段代码中:

当我们输入 12,2的时候,结果如下

image-20230915130311217

当我们输入2 , 0的时候结果如下

image-20230915130333075

我们这段程序的运行逻辑实际上是这样的:

一开始都是按照正常的顺序逻辑进行执行的,当运行过程中遇到了throw标识符的时候,这个throw会抛出一个字符串、数字、甚至一个对象等等时候,我们将抛出的这个东西称为异常。一旦抛出异常,这个程序会瞬间跳转到匹配的catch块中。然后执行catch块中的逻辑。所以我们上面的代码才会出现如上的情况。抛异常的过程中会将跳出之前的栈帧全部结束掉,但是执行流是直接跳过去的。

四、异常的抛出与捕获

1.异常的抛出原则

  1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。

  2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。

  3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)

  4. catch(…)可以捕获任意类型的异常,问题是不知道异常错误是什么。

  5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用。

2.在函数调用链中异常栈展开匹配原则

  1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。

  2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。

  3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(…)捕获任意类型的异
    常,否则当有异常没捕获,程序就会直接终止。

  4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

当try里面还嵌套了一层try的时候,即在里面已经被捕获了。外面就不会在捕获了。

image-20230915134827726

在我们抛出string等对象的时候,我们可能会疑惑,这个string出了作用域不会 销毁吗?其实不是的,在中间会产生一个临时对象。这个临时对象在在catch以后才会销毁。

image-20230915140722776

但是在我们日常的使用中,我们经常会遇到某个异常我们没有捕获,这就导致程序直接挂了。为了避免这种情况,我们就会使用…来进行捕获异常,它的作用是任意类型的异常都会被捕获。image-20230915141932912

不过由于这个…任意类型都能捕获,所以它一定要写在最后面。否则它会将全部异常都捕获进去。我们可以注意到,下面的程序已经报错。

image-20230915142146559

五、实际应用中的异常使用

在我们实际应用中,单纯的抛出一个数字或者字符串并没有什么大的意义。我们一般都会抛出一个自定义类型。这个类型一般有一个id和一个错误信息。

class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{}
	virtual string what() const
	{
		return _errmsg;
	}
protected:
	string _errmsg;
	int _id;
};

比如说,我们的1号错误是没有权限、2号错误是服务器故障、3号错误是网络错误等等。

比如下面的就是我们要发送一个消息,我们如果发送失败的话,看一下是不是网络错误,如果是的话,continue重新发送一次,如果不是那么记录日志然后结束。

void seed()
{
	while (n--)
	{
		try
		{
			SeedMsg(msg);
		}
		catch (const Exception& e)
		{
			if (e.getid == 3)
			{
				continue;
			}
			else
			{
				//记录日志
				logging();
				break;
			}
		}
	}
}

不过实践中要比上面的远远复杂。因为需要很多模块的问题:如网络模块、缓存模块、数据库模块等等。

而我们每个模块都有每个模块的异常的方式。我们不可能就只使用一个结构体,这样太大了。所以我们就会使用抛出派生类对象,使用基类捕获。

如下是一个简易的服务器中的异常实现

class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{}
	virtual string what() const
	{
		return _errmsg;
	}
protected:
	string _errmsg;
	int _id;
};
class SqlException : public Exception
{
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;
};
class CacheException : public Exception
{
public:
	CacheException(const string& errmsg, int id)
		:Exception(errmsg, id)
	{}
	virtual string what() const
	{
		string str = "CacheException:";
		str += _errmsg;
		return str;
	}
};
class HttpServerException : public Exception
{
public:
	HttpServerException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id)
		, _type(type)
	{}
	virtual string what() const
	{
		string str = "HttpServerException:";
		str += _type;
		str += ":";
		str += _errmsg;
		return str;
	}
private:
	const string _type;
};
void SQLMgr()
{
	srand(time(0));
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}
	cout << "执行成功" << endl;
}
void CacheMgr()
{
	srand(time(0));
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}
	SQLMgr();
}
void HttpServer()
{
	// ...
	srand(time(0));
	if (rand() % 3 == 0)
	{
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}
	CacheMgr();
}
int main()
{
	while (1)
	{
		Sleep(500);
		try
		{
			HttpServer();
		}
		catch (const Exception& e) // 这里捕获父类对象就可以
		{
			// 多态
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}

image-20230915165955212

如上所示,我们就可以将每一个异常都给记录下来。虽然我们可以抛任意类型,但是我们还是要抛派生类。

还有必须要写…处理其他异常,防止有人忘记写继承,导致未知错误。如下图所示

image-20230915172526744

六、C++标准库的异常体系

C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:

image-20230915173229060

这些类中exception是基类,其他都是派生类,所以我们只需要传一个exception就可以了

同时这也是C++标准库里面exception的定义

image-20230915173423449

image-20230915173556709

image-20230915173731175

其中下面红色的是稍微常见一点的

image-20230915173923698

如下是c++标准库里面的一些异常的使用。

int main()
{
	try 
    {
		vector<int> v(10, 5);
		// 这里如果系统内存不够也会抛异常
		v.reserve(1000000000);
		// 这里越界会抛异常
		v.at(10) = 100;
	}
	catch (const exception& e) // 这里捕获父类对象就可以
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception" << endl;
	}
	return 0;
}

七、异常规范

在实际中,异常也有它的缺陷,那就是会导致执行流乱跳。是想一下,假如某个异常被套了好几层函数,一旦抛了异常,就会出现很大的问题

  1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。

  2. 函数的后面接throw(),表示函数不抛异常。

  3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。

不过这个规范并不强制。

// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出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 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

八、异常安全

  • 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
  • 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
  • C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题。

比如如下的情况就出现了内存泄漏

image-20230915180832769

为了解决上面的问题,我们可以在在Fuc中在捕获一次异常,拦截下来释放了内存然后在此抛出异常

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (double)a / (double)b;
}
void Func()
{
	int* array = new int[10];
	int len, time;
	cin >> len >> time;
	try
	{
		cout << Division(len, time) << endl;
	}
	catch (const char* errmsg)
	{
		cout << "delete []" << array << endl;
		delete[] array;
		throw errmsg;
	}
	cout << "delete []" << array << endl;
	delete[] array;
	
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	return 0;
}

image-20230915181244135

当然上面仅仅只是一种方式,我们还有一种方式是使用…,然后直接throw,这种方式是捕获什么抛出什么

void Func()
{
	int* array = new int[10];
	int len, time;
	cin >> len >> time;
	try
	{
		cout << Division(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array << endl;
		delete[] array;
		throw;
	}
	cout << "delete []" << array << endl;
	delete[] array;
}

image-20230915181403418

上面的还仅仅只是只有一种资源的情况,还有更复杂的一种情况。比如如下的情况

int* p1;
int* p2;

func();

delete p1;
delete p2;

上面是有两种内存需要释放,如果p1抛出了异常,那么就只需要清理p1的资源

如果p2抛出了异常,那么就需要清理p1和p2的资源,一旦资源更多,就变得更加复杂

这里就需要使用智能指针去解决了。

九、异常的优缺点

C++异常的优点:

  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug 。

  2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误。

  3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。

  4. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

C++异常的缺点:

  1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。

  2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。

  3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。

  4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。

  5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化。

总结

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

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

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

相关文章

计网第五章(运输层)(四)(TCP的流量控制)

一、基本概念 流量控制就是指让发送方的发送速率不要太快&#xff0c;使得接收方来得及接收。可以使用滑动窗口机制在TCP连接上实现对发送方的流量控制。 注意&#xff1a;之前在讨论可靠传输时&#xff0c;讨论过选择重传协议和回退N帧协议都是基于滑动窗口的机制上进行实现…

学生在线查询系统

在教育管理中&#xff0c;学生查询系统是一个必不可少的工具&#xff0c;它能够方便学生、家长和教师快速获取学生的各项信息。而易查分作为一个功能强大的在线查询工具&#xff0c;能够帮助教育机构快速搭建一个高效便捷的学生查询系统。通过注册易查分账号&#xff0c;创建查…

Java毕业设计 SSM SpringBoot 水果蔬菜商城

Java毕业设计 SSM SpringBoot 水果蔬菜商城 SSM 水果蔬菜商城 功能介绍 首页 图片轮播 关键字搜索商品 分类菜单 折扣大促销商品 热门商品 商品详情 商品评价 收藏 加入购物车 公告 留言 登录 注册 我的购物车 结算 个人中心 我的订单 商品收藏 修改密码 后台管理 登录 商品…

element ui - el-table 表头筛选

element ui - el-table 表头筛选 前言**场景**&#xff1a;根据表头筛选出表格中符合条件的数据&#xff1b;**效果**&#xff1a; 情况一&#xff1a;表格没有分页方法代码 前言 场景&#xff1a;根据表头筛选出表格中符合条件的数据&#xff1b; 效果&#xff1a; 筛选结果…

代码随想录--栈与队列-用栈实现队列

使用栈实现队列的下列操作&#xff1a; push(x) -- 将一个元素放入队列的尾部。 pop() -- 从队列首部移除元素。 peek() -- 返回队列首部的元素。 empty() -- 返回队列是否为空。 需要两个栈一个输入栈&#xff0c;一个输出栈&#xff0c;这里要注意输入栈和输出栈的关系。 i…

CSDN中,如何创建目录或标题

创建目录或标题 1.复制&#xff0c;自动生成目录2.复制&#xff0c;自动生成标题3.CSDN标准写法如下图 1.复制&#xff0c;自动生成目录 [TOC]或 [TOC](这里写目录标题) # 一级目录 ## 二级目录 ### 三级目录2.复制&#xff0c;自动生成标题 # 一级目录 ## 二级目录 ### 三级目…

Java 多种获取项目路径下的文件

目标文件放在项目的resources文件夹下 的 mytxt文件里面&#xff0c;文件名叫 file Test.txt&#xff1a; 其实可以看到&#xff0c;项目运行后&#xff0c;这个文件被丢到了target文件夹下&#xff1a; 拿到这个文件的 InputStream &#xff1a; 比如我们在FileUtil里面写个获…

懒人制作企业期刊的秘籍

企业期刊是展示企业文化、提升形象、传递信息的重要工具。但是&#xff0c;制作企业期刊需要投入大量的时间和精力&#xff0c;对于忙碌的企业来说是一项艰巨的任务。 所以肯定也有人需要一款不会花费大量时间就能制作出高级感的企业期刊&#xff0c;大家不妨试试FLBOOK在线制…

Feign远程接口调用

概述 目的&#xff1a;解决微服务调用问题。如何从微服务A调用微服务B提供的接口。 特性&#xff1a; 声明式语法&#xff0c;简化接口调用代码开发。像调用本地方法一样调用其他微服务中的接口。集成了Eureka服务发现&#xff0c;可以从注册中心中发现微服务。集成了Spring…

SpringBoot:返回响应,统一封装

说明 接口的返回响应&#xff0c;封装成统一的数据格式&#xff0c;再返回给前端。 返回响应&#xff0c;统一封装实体&#xff0c;数据结构如下。 代码 package com.example.core.model;import io.swagger.v3.oas.annotations.media.Schema; import lombok.*;/*** 返回响应…

英飞凌TC3xx--深度手撕HSM安全启动(四)--TC3xx HSM使能和配置技巧

上一章,我们简单聊了下英飞凌TC3xx的HSM的系统框架、相关UCB、Host和HSM通信模块。今天着重分析HSM的使能。 1. 系统引入HSM的思考 为什么要增加HSM 信息安全方面考虑,系统的安全启动、ECU之间安全数据的交互、ECU内部的敏感信息保存 TC3xx使能HSM后,HSM的代码应该…

spring aop源码解析

spring知识回顾 spring的两个重要功能&#xff1a;IOC、AOP&#xff0c;在ioc容器的初始化过程中&#xff0c;会触发2种处理器的调用&#xff0c; 前置处理器(BeanFactoryPostProcessor)后置处理器(BeanPostProcessor)。 前置处理器的调用时机是在容器基本创建完成时&#xff…

安防监控系统/视频云存储/视频AI智能分析:人形检测算法应用汇总

随着人工智能的飞速发展&#xff0c;TSINGSEE青犀智能AI算法功能也日渐丰富&#xff0c;除了常见的人脸、工服、安全帽检测以外&#xff0c;人形检测算法的应用也十分广泛&#xff0c;主要可以应用在以下场景&#xff1a; 1、安防监控系统 人形检测算法可以应用于监控摄像头中…

ChatGPT OpenAI 针对HR与财务岗位一键核对工资表差异

HR人力资源与财务部门关于奖金的计算,两个部门计算的结果有差异如何将差异内容显示。 如何快速找出不相同的单元格。 我们给ChatGPT来提出需求来解决。 prompt: 请写出一个VBA程序找出E3:E12单元格区域与E16:E25单元格区域中不相同的单元格,并填充为红色背景显示,请写出完…

23062QTday1

自己制作一个登录界面 头文件&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget>#include <QApplication>#include <QLineEdit> #include <QLabel> #include <QMovie> class Widget : public QWidget {Q_OBJECTpublic:Widget(…

概率统计笔记:从韦恩图的角度区分 条件概率和联合概率

联合概率&#xff1a;两个或多个事件同时发生的概率。用 P(A∩B) 或 P(A,B) 表示 条件概率&#xff1a;在已知某个事件发生的条件下&#xff0c;另一个事件发生的概率。用P(A∣B) 表示在事件 B 发生的条件下&#xff0c;事件 A 发生的概率。 不难发现联合概率的样本空间更大&am…

多线程|多进程|高并发网络编程

一.多进程并发服务器 多进程并发服务器是一种经典的服务器架构&#xff0c;它通过创建多个子进程来处理客户端连接&#xff0c;从而实现并发处理多个客户端请求的能力。 概念&#xff1a; 服务器启动时&#xff0c;创建主进程&#xff0c;并绑定监听端口。当有客户端连接请求…

华为云云耀云服务器L实例评测 | 搭建docker环境

目录 &#x1f352;docker的概念 &#x1f352;Docker 的优点 &#x1fad0;1、快速&#xff0c;一致地交付您的应用程序 &#x1fad0;2、响应式部署和扩展 &#x1fad0;3、在同一硬件上运行更多工作负载 &#x1f352;云耀云服务器L实例 &#x1fad0;产品优势 &#x1f95d…

如何使用反 CSRF 令牌保护您的网站和 Web 应用程序

防止跨站点请求伪造攻击 (CSRF/XSRF)的最常见方法是使用反 CSRF 令牌&#xff0c;该令牌只是一个唯一值集&#xff0c;然后由 Web 应用程序需要。CSRF 是一种客户端攻击&#xff0c;可用于将用户重定向到恶意网站、窃取敏感信息或在用户会话中执行其他操作。幸运的是&#xff0…

组件自定义事件学习笔记

组件自定义事件_绑定 JS中有内置事件比如click&#xff0c;keyup。内置事件是给标签使用的&#xff0c;而自定义事件是给组件使用的。 子组件给父组件传递数据有两种方式 App父组件&#xff0c;School和Student是子组件。 子组件给父组件传递函数类型的props实现&#xff…