C++-->异常

1、异常的概念以及使用

1.1 异常的概念

异常处理机制是允许程序中独立开发的部分出现问题时,进行相应的处理。异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后在另一部分进行问题的处理。

对比C++的异常机制,C语言是通过错误代码的形式处理错误,例如出现越界或者益出等等是通过对错误信息进行分类编号,拿到编号后去查询错误信息,太麻烦了。而C++的异常可以抛出一个对象,这个对象可以涵盖更全面的信息。

1.2 异常的抛出和捕获

(1)程序出现问题的时候,我们通过一个关键字throw,抛出一个对象来引发异常,抛出的异常用catch来捕获,有一点需要注意的就是throw出的对象和catch捕获对象的类型要相同,如下代码所示:

#include<iostream>
using namespace std;
int main()
{
	while (1)
	{
		int input = 0;
		cin >> input;
		try
		{
			if (input >= 0)
			{
				cout << "数值大于0:>" << input << endl;
			}
			else
			{
				throw string("异常已被抛出,数值小于0");
			}
		}

		catch (const string& _errmsg)
		{
			cout << _errmsg << endl;
		}
	}
	return 0;
}

👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇

运行结果为:


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

(3)当throw执行的时候,在throw后面的代码都不会执行了,而是直接跳到匹配的catch的代码段进行执行,catch可能是同一个函数内的局部的catch,也有可能是调用链中另一个函数的catch。如下代码所示:

#define _CRT_SECURE_NO_WARNINGS 
#include<iostream>
#include<string>
#include<exception>
using namespace std;

double Divide(int a, int b)
{
	try
	{
		//当b==0的时候抛出异常
		if (b == 0)
		{
			string s("Divide by zero condition");
			throw s;
			cout << "666" << endl;
		}
		else
		{
			return((double)a / (double)b);
		}
	}
	//这个不符合所以抛出的异常不会被这个接受
	catch (int errid)
	{
		cout <<"Divide函数内的catch" << errid << endl;
	}

	return 0;
}

void Func()
{
	int len, time;
	cin >> len >> time;

	try
	{
		cout << Divide(len, time) << endl;
	}
	catch(const char* errmsg)
	{
		cout << "Func函数内的catch" << errmsg << endl;
	}

	cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}

int main()
{
	while (1)
	{
		try
		{
			Func();
		}
		catch (const string& errmsg)
		{
			cout << "main函数的catch:>"<<errmsg << endl;
		}
	}
	return 0;
}

👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇

运行结果为:

我们看到,上面throw有一个cout<<"6666"<<endl;但一直都没执行到,因为执行到throw的语句的时候就会直接跳到匹配的catch语句处。

然后第二点就是catch函数不一定和throw函数在同一个函数域里面。


(4)抛出异常对象后,出产生一个临时的异常对象的拷贝,因为抛出的对象可能是一个局部对象,出了这个函数这个栈帧就被销毁,那么就会出问题,所以会抛出一个拷贝的临时对象。


1.3 栈展开

  • 抛出异常后,程序暂停当前函数的执行,开始找与throw参数匹配的catch子句,首先检查throw本身是否在try的内部,如果在则查找匹配的catch语句,如果有匹配到就跳转到catch处。
  • 如果throw在当前的函数没有找到适配的catch,或者找到的catch类型不匹配,则退出当前的函数继续在外层函数进行查找,上述查找catch的过程称为栈展开。
  • 如果到达main函数也还是没有找到适配的,程序就会调用标准库的terminate函数终止程序。如下代码所示:
    #define _CRT_SECURE_NO_WARNINGS 
    #include<iostream>
    #include<string>
    #include<exception>
    using namespace std;
    
    double Divide(int a, int b)
    {
    	try
    	{
    		//当b==0的时候抛出异常
    		if (b == 0)
    		{
    			string s("Divide by zero condition");
    			throw s;
    			cout << "666" << endl;
    		}
    		else
    		{
    			return((double)a / (double)b);
    		}
    	}
    	//这个不符合所以抛出的异常不会被这个接受
    	catch (int errid)
    	{
    		cout <<"Divide函数内的catch" << errid << endl;
    	}
    
    	return 0;
    }
    
    void Func()
    {
    	int len, time;
    	cin >> len >> time;
    
    	try
    	{
    		cout << Divide(len, time) << endl;
    	}
    	catch(const char* errmsg)
    	{
    		cout << "Func函数内的catch" << errmsg << endl;
    	}
    
    	cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
    }
    
    int main()
    {
    	while (1)
    	{
    		try
    		{
    			Func();
    		}
            //这个改成不符合的。
    		catch (const char& errmsg)
    		{
    			cout << "main函数的catch:>"<<errmsg << endl;
    		}
    	}
    	return 0;
    }

    👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇

    运行结果为:

下图是对上面的详细介绍:


1.4 查找匹配的处理代码

1、一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配,那就会匹配到最近的那个类型处。如下代码所示:

#include<iostream>
using namespace std;
int main()
{
	try
	{
		while (1)
		{
			int input = 0;
			cin >> input;
			try
			{
				if (input >= 0)
				{
					cout << "数值大于或者等于0:>" << input << endl;
				}
				else
				{
					throw string("异常已被抛出,数值小于0");
				}
			}
			catch (const string& _errmsg)
			{
				cout << "我是内层接受异常的:>" << _errmsg << endl;
			}
		}
	}
	catch (const string& _errmsg)
	{
		cout << "我是最外层接受异常的:>" << _errmsg << endl;
	}
	return 0;
}

👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇

运行结果为:


2、但有些例外,就是允许非常量向常量的类型转换,也就是权限缩小;允许从派生类向基类类型的转换,这个才是最实用的点。如下代码所示:

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

	virtual string what()const
	{
		return _errmsg;
	}

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

class DelException : public Exception
{
public:
	DelException(const string& errmsg, int id, const string& Del)
		:Exception(errmsg, id)
		,_Del(Del)
	{}

private:
	const string _Del;
};

void Del(int num)
{
	if (num % 5 == 0)
	{
		throw DelException("你被删除了", 100, "Delete");
	}

	else if (num % 4 == 0)
	{
		throw DelException("你被拉黑了", 404, "lahei");
	}
	else
	{
		cout << "发送成功" << endl;
	}
}
int main()
{
	int input = 0;
	cin >> input;
	try
	{
		Del(input);
	}
	catch (const Exception& e)
	{
		cout << e.what() << endl;
	}

	//如果不捕获整个程序就会挂掉。

	catch (...)
	{
		cout << "Unkown Exception" << endl;
	}
	return 0;
}

首先我们先来介绍一下这个代码,这里模拟的是一个人在微信里把你删了或者是拉黑了,而我们要抛出的内容不同。首先我们定义一个Exception的基类,有成员变量_errmsg用来接收抛出的异常,然后_id就是错误id,在网络里我们可能见过一些网站的404啥的,这也是个错误id,404代表着网站丢失。然后我们用一个DelException进行继承,作为派生类,然后定义多了一个_Del进行接受信息即是被删除了还是被拉黑了。然后Del函数就是接受一个数据如果符合两个条件的其中一个则抛出相应的异常,如果都不符合则说明发送成功。

然后是catch,这里有两个catch,下面的那个“...”我们先不理,我们先看第一个catch,这个catch的参数是基类,我们知道基类能接受派生类对象,然后是e.what,如果传入的是基类,则调用基类的what,传入的是派生类,则调用的是派生类的what,这个在我们前面继承已经讲过,即“指向谁就调用谁”。

👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇

运行结果为:


3、如果到main函数,异常throw依旧没有匹配就会终止程序,我们上面讲过,但终止程序是一个很危险的事情,你想想看,例如csgo的服务器突然终止了,那么在打游戏的人的数据就会全部丢失,有的人赢了最后一把就能上S了,但是throw没有匹配,终止了服务器,这是多么大的问题,所以我们可以使用一个catch(...)进行保底操作,就是不管你throw是什么类型都能接受。


1.5 异常的重新抛出

这个异常的重新抛出也很重要,再拿微信举个例子,例如发送消息的时候,在我们网络不好的时候会出现一直转圈,但按照计算机的速度,这一点点网络真的至于说让信息转几秒圈才发出去吗,其实并不然,出现转圈是说明网络断开了,发不出去,然后就把异常重新抛出,再对发信息这个函数进行重试。

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

	virtual string what()const
	{
		return _errmsg;
	}

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

class DelException : public Exception
{
public:
	DelException(const string& errmsg, int id, const string& Del)
		:Exception(errmsg, id)
		, _Del(Del)
	{}

	virtual string what() const
	{
		string str = _errmsg;
		return str;
	}

private:
	const string _Del;
};
//
// 下面程序模拟展示了聊天时发送消息,发送失败补货异常,但是可能在
// 电梯地下室等场景手机信号不好,则需要多次尝试,如果多次尝试都发
// 送不出去,则就需要捕获异常再重新抛出,其次如果不是网络差导致的
// 错误,捕获后也要重新抛出。
void _SendMsg(const string& s)
{
	if (rand() % 2 == 0)
	{
		throw DelException("网络不稳定,发送失败", 102, "put");
	}
	else if (rand() % 7 == 0)
	{
		throw DelException("你已经不是对象的好友,发送失败", 103, "put");
	}
	else
	{
		cout << "发送成功" << endl;
	}
}

void SendMsg(const string& s)
{
	// 发送消息失败,则再重试3次
	for (size_t i = 0; i < 4; i++)
	{
		try
		{
			_SendMsg(s);
			break;
		}
		catch (const Exception& e)
		{
			// 捕获异常,if中是102号错误,网络不稳定,则重新发送
			// 捕获异常,else中不是102号错误,则将异常重新抛出
			if (e.getid() == 102)
			{
				// 重试三次以后否失败了,则说明网络太差了,重新抛出异常
				if (i == 3)
					throw;

				cout << "开始第" << i + 1 << "重试" << endl;
			}
			else
			{
				// 重新抛出
				throw;
			}
		}
	}
}

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

	return 0;
}

👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇

运行结果为:

至于为什么要重新抛出那是因为可能要在外边的catch进行处理异常。


1.6 异常的安全问题

正如我们前面说过,出现异常后执行了throw就会发生,throw就会去匹配定义的catch,而throw后面的代码就不再执行了,但如果前面申请了资源,后面进行释放,但是中间可能抛异常就会导致资源没有进行释放,这里就由于异常引发了资源泄露,产生安全性的问题。中间我们还需要重新捕获异常然后再抛出。而后面智能指针能很好的解决这个问题。

double Divide(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (double)a / (double)b;
}
void Func()
{
	// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array没有得到释放。
	// 所以这⾥捕获异常后并不处理异常,异常还是交给外层处理,这⾥捕获了再
	// 重新抛出去。
	int* array = new int[10];
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	}
	catch (...)
	{
		// 捕获异常释放内存
		cout << "delete []" << array << endl;
		delete[] array;
		throw; // 异常重新抛出,捕获到什么抛出什么
	}
	cout << "delete []" << array << endl;
	delete[] array;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception" << endl;
	}
	return 0;
}

1.7 异常规范

对于用户和编译器而言,预先知道某个程序会不会抛出异常有很大的益处,知道某个函数是否会抛出异常有助于简化调用函数的代码。

C++11添加多了个关键字noexcept,在函数参数列表后面加noexcept表示不会抛异常。

但是有个问题就是编译器并不会在编译时检查noexcept,也就是说如果一个函数用noexcep修饰了,但又同时包含了throw语句或者调用的函数还会抛出异常的话,编译器还是会顺利编译通过。但是声明了noexcept的函数抛出了异常,程序会调用terminate终止程序。

noexcept还可以作为一个运算符去检测一个表达式是否抛出异常,可能则会返回false,不会就返回true。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值