【异常】—— 我与C++的不解之缘(二十四)

一、异常是什么?

异常,这个十分陌生的名词;

试想一下,在我们之前写代码的过程中,程序运行出现了一些问题(就比如AVL树更新平衡因子的过程中,平衡因子出现了不可能的现象,这说明这个AVL树存在问题;)但是我们之前只是单纯的让程序终止,但是在以后的实践中,程序是一直运行的,所以我们不能直接将程序直接终止。

这是,我们应该做的是:对出现的问题进行处理,并且程序不能够终止

1. C语言传统的处理方式

这里再看一下C语言传统的处理方式

  • 终止程序,assert
  • 返回错误码

这两种无论是哪一种处理方式都是返回错误码,部分情况下使用终止程序来处理非常严重的错误。

2. C++异常概念

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

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

二、异常使用

1. 异常的抛出和捕获

  • 程序出问题时,我们通过throw抛出一个对象来引发异常,该对象的类型以及当前调用链就决定了应该由哪一个catch的处理代码来处理该异常。
  • 被选中的处理代码catch块是调用链中与该类对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出部分告知异常处理部分发生了什么错误。
  • throw执行的时候,throw后面的语句将不再被执行;程序就从throw位置跳到与之匹配的catch块(catch可以是同一个函数内的,也可能是调用链中另一个函数的catch),简单来说有两种含义就是(1、沿着调用链的函数可能提前退出;2、`一旦程序开始执行异常处理程序,沿着调用链创还能的对象都要被销毁)。
  • 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象是一个局部对象,所以会生成一个拷贝对象,这个拷贝对象在catch语句后销毁。类似与传值返回

这么多概念,看起来好麻烦,这里看一段代码理解一下

double Divide(int x, int y)
{
	try
	{
		if (y == 0)
		{
			throw string("this divisor is zero");//这里直接使用临时对象
		}
		else
		{
			return (double)x / y;
		}
	}
	catch (int y)
	{
		cout << "y:" << y << endl;
	}
}
void func(int a)
{
	try
	{
		if (a == 3)
		{
			throw string("a == 3");
		}
		int x, y;
		cin >> x >> y;
		Divide(x, y);
	}
	catch (double d)
	{
		cout << "d:" << d << endl;
	}
}
int main()
{
	int a;
	cin >> a;
	try {
		func(a);
	}
	catch(string str)
	{
		cout << str << endl;
	}
	catch (char ch)
	{
		cout << ch << endl;
	}
	catch (...)
	{
		cout << "Unknown exception" << endl;
	}
}

这里补充:我们不能够用多个catch完全匹配所以的异常类型,使用catch(...)来匹配所有的异常对象类型。

输入**3**,输出:

a == 3

输入1 3 0,输出:

this divisor is zero

简单理解,在我们平常练习写代码时,基本上用不到;在未来项目实践中才会有所使用。

2. 栈展开

  • 抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch语句,首先就要检查throw本身是否在try内部,如果在就查找匹配的catch语句,如果有就跳转。
  • 如果当前函数中没有try/catch语句,或者当前函数内的catch类型不匹配,就会退出当前函数,在外层调用函数链中查找,整个过程叫做栈展开。
  • 如果栈展开整个过程到达了main函数,依然没有找到匹配的catch语句,程序就会调用标准库的terminate函数终止程序。

在这里插入图片描述

3. 查找匹配的处理代码

  • 一般情况下,抛出异常对象和catch类型是完全匹配的,如果有多个匹配的类型,就选择调用链中最近的一个。
  • 也存在也写例外,比如非常量向常量的类型转换数组向数组元素指针类型的转换函数转换成函数指针允许派生类向基类类型的转换(这个非常实用)。
  • 如果栈展开到main函数,异常依旧没有匹配就会终止程序,不是有严重错误的情况下,我们不希望程序终止,所以我们一般在main函数中使用catch(...),它捕获所有异常,但是不知道异常错误是什么。

4.异常的重新抛出问题

对于异常重新抛出问题,大致意思就是:

有时catch匹配到异常对象后,需要对错误进行分类,再对其中异常错误进行特殊的处理,其他的错误就要重新抛出;将问题抛出到外层调用链处理。

用法:就直接抛出throw;即可。

这里模拟一下发送信息这样的场景:

#include<thread>

class Exception
{
public:
	Exception(const string& str, const int& id)
		:_errmsg(str)
		, _id(id)
	{}

	virtual string what() const
	{
		return _errmsg;
	}
	int getid() const
	{
		return _id;
	}
protected:
	string _errmsg;
	int _id;
};

class HttpException :public Exception
{
public:
	HttpException(const string& str, const int& id, const string type)
		:Exception(str, id)
		, string_type(type)
	{}
	virtual string what() const
	{
		string ret = "HttpException:";
		ret += string_type;
		ret += ":";
		ret += _errmsg;
		return ret;
	}
private:
	string string_type;
};
void _seedMsg(const string str)
{
	if (rand() % 2 == 0)
	{
		throw HttpException("网络错误,请稍后重试", 101, "put");
	}
	else if (rand() % 7 == 0)
	{
		throw HttpException("你已经不是对方好友,发送失败", 102, "put");
	}
	else
	{
		cout << "发送成功" << endl;
	}
}

void SeedMsg(const string& str)
{
	for (int i = 0; i < 4; i++)
	{
		try
		{
			_seedMsg(str);
			break;
		}
		catch (const Exception& e)
		{
			if (e.getid() == 101)
			{
				//网络问题
				if (i == 3)
					throw;
				cout << "开始第" << i + 1 << "次尝试" << endl;
			}
			else
			{
				throw;
			}
		}
	}

}
int main()
{
	srand(time(0));
	string str;
	while (cin >> str)
	{
		try
		{
			SeedMsg(str);
		}
		catch (const Exception& e)
		{
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}

	return 0;
}
  • 这里就发送信息,然后随机(发送成功或者失败);发送失败有两种原因(一是不是对方好友,二是网络问题);如果网络问题则进行重新发送,重试三次则不在尝试。
  • 这里就用到异常

5.异常的安全问题

在之前,我们程序是从上到下顺序执行的;但是如果抛出异常之后,程序就会跳转到异常匹配catch的位置,而抛异常throw后面的代码就不再执行;那这里就有一些问题:

  • 在抛出异常之前申请的空间资源,会不会进行释放?
  • 析构函数中如果抛异常,那会不会析构不完全?

我们先来看第一个问题

double Divide(int x, int y)
{
	if (y == 0)
	{
		throw string("the y is zero");
	}
	return (double)x / double(y);
}
void test(int x, int y)
{
	int* arr = new int[10];
	try
	{
		Divide(x, y);
	}
	catch (const int& i)
	{
		cout << i << endl;
	}
	
	delete[] arr;
	cout << "delete[] arr" << endl;
}

int main()
{
	int x, y;
	cin >> x >> y;
	while (1)
	{
		try
		{
			test(x, y);
		}
		catch (const string& str)
		{
			cout << str << endl;
		}
		catch (...)
		{
			cout << "unknown exception" << endl;
		}
		cin >> x >> y;
	}
	return 0;
}

这里在Divide函数中,如果y==0就会抛出异常,并且在该函数内没有捕获该异常,就继续将异常抛给外层调用的函数test,在testnew了一个int数组,但是没有捕获Divide函数抛出的异常,程序直接接跳到main函数当中去,就导致申请的空间资源没有被释放。

输入:(这里为了方便就写了死循环)

2 3
2 0

输出:

delete[] arr
the y is zero

这里确实造成了空间泄露的问题,那这个问题如何解决呢?

  • 这里,我们在test函数中,调用Divide函数并且进行异常捕获,捕获之后释放空间资源再将异常抛出;
void test(int x, int y)
{
	int* arr = new int[10];
	try
	{
		Divide(x, y);
	}
	catch (...)
	{
		delete[] arr;
		cout << "delete[] arr" << endl;
		throw;
	}

}

这里看似问题已经解决了,但是还存在新的问题;

如果这里申请了多个空间资源呢?

就比如

void test(int x, int y)
{
	int* arr1 = new int[10];
	int* arr2 = new int[10];
	try
	{
		Divide(x, y);
	}
	catch (...)
	{
		delete[] arr1;
		cout << "delete[] arr1" << endl;
		delete[] arr2;
		cout << "delete[] arr2" << endl;
		throw;
	}
}

这样看起来貌似没有什么问题,但是如果第二个new抛异常了呢?

对于这个疑问,在后面智能指针再来探讨。

对于第二个问题,在析构函数中尽量不出现抛异常。

如果析构函数需要释放十个,但是到第5个抛异常了,就需要捕获异常,导致后面资源释放不完全,造成资源泄露。

《Effctive C++》中也讲了,别让异常逃离析构函数。

三、标准库的异常

https://legacy.cplusplus.com/reference/exception/exception/?kw=exception

C++标准库中定义了自己的一套异常继承体系库,基类是exception,所以我们在程序中只需要在主函数中捕获exception即可,要获取异常的具体信息直接调用what函数即可;而且what是虚函数。

在这里插入图片描述

这里就不详细介绍了。

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

迟来的grown

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值