C++异常

异常的概念

异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后将解决问题的任务传递给程序的另一部分,检测环节无须知道问题的处理模块的所有细节

异常是众多面向对象的语言的一种解决错误的方式,它对比的是c语言处理错误的方式,c语言处理错误的方式一般是返回错误码,错误码的本质就是对错误信息的一种编号。对C++来说异常就比如,当new空间new失败时,就会抛异常,它返回的不是错误码,返回的是一个对象,这个对象包含错误相关的一些信息。还有就是,c语言在出现错误时,是调用到错误的当前那一层处理,也就是外层处理错误。对异常来说,在出现错误时,可以跳跃几层去处理问题。可以理解为出现问题和解决问题的位置离得很远

异常的抛出(throw)和捕获(catch)

  • 程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前的调用链决定了应该由哪一个catch的处理代码来处理异常

比如:当main函数调用f1,f1函数调用f2,f2函数调用f3时,若f3抛出了一个异常,不一定是由调用它的f2函数来处理,要根据抛出的对象的类型及调用链决定

  • 被选中的catch是调用链中与抛出对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误

代码举例:

#include <iostream>
#include<string>
using namespace std;

double Divide(int a, int b)
{
	try
	{
		//b == 0时抛出异常
		if (b == 0)
		{
			string s("Divide by zero condition!");
			throw s;
		}
		else
		{
			return ((double)a / (double)b);
		}
	}
	catch(int errid) //捕获的对象的类型是int
	{
		cout << "errid" << endl;
	}
	return 0;
}

void func()
{
	int len, time;
	cin >> len >> time;
	try
	{
		cout << Divide(len, time) << endl;
	}
	catch (const char* errmsg)//捕获对象的类型是char*
	{
		cout << errmsg << endl;
	}
	
	cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
	//若抛出的异常与该函数内部的catch匹配,则会往下走该条指令,或没有异常try之后也会走该条指令
}

int main()
{
	while (1)
	{
		try
		{
			func();
		}
		catch (const string& errmsg)
		{
			cout << errmsg << endl;
		}
	}
	return 0;
}

上面的代码分析,当调用func函数,输入值,然后调用Divide函数,若b为0时,就会抛出一个异常,上述代码中抛出是一个string对象,那由哪个catch来捕获呢,是Divide函数中的catch来捕获吗,不是的,根据C++的捕获规则,是由与抛出对象类型匹配以及是离抛出异常位置最近的catch来捕获,那么上面的代码中,符合规则的只有main函数中的catch。所以当抛出异常时,编译器就会先在当前层寻找匹配的catch,没有匹配就再往上一层,找到了就会从找到的catch位置开始继续往下执行,若找到main函数了还没有找到,就会终止程序
在这里插入图片描述
注:这里的“寻找”只是形象的说法,其实在编译时就已经确定好了

  • 当throw执行时,throw后面的语句将不再被执行。程序的执行从throw位置跳到与之匹配的catch模块,catch可能是同一层函数中的一个局部的catch,也可能是调用链中另一个函数的catch,控制权从throw位置转移到了catch位置。还有两个重要的含义:1.当抛出异常时,沿着调用链的函数可能提早退出。2.一旦程序开始执行异常处理程序,沿着调用链创建出来的对象都将被销毁,当然这里的对象都会调用对应的析构来销毁
  • 抛出异常对象后,会生成异常对象的拷贝,因为抛出异常的对象可能是一个局部对象,对于局部对象它的生命周期是不会变的,所以会生成一个拷贝对象,这个拷贝对象会在catch之后销毁(类似于函数的传值返回)

“栈展开”

  • 抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理
  • 如果当前函数中没有try/catch子句,或者有但类型不匹配,则退出当前函数,继续在外层调用函数链中寻找,上述寻找catch的过程被称为栈展开
  • 若到达main函数,依旧没有匹配的catch子句,程序会调用标准的terminate函数终止程序,说明抛出了异常,一定是要被捕获的
  • 如果找到匹配的catch子句处理后,catch子句后的代码会继续执行

查找匹配的处理代码

  • 若到main函数,异常仍旧没有被捕获,就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般函数中最后都会使用catch(…),它可以捕获任意类型的异常,但是不知道异常的错误是什么

若将上面举例代码的main函数改为下面这样:

int main()
{
	while (1)
	{
		try
		{
			func();
		}
		catch (const char* errmsg) //类型改成const char*
		{
			cout << errmsg << endl;
		}
		catch (...)
		{
			cout << "未知异常" << endl;
		}
	}
	return 0;
}

在这里插入图片描述

将main函数改成上述代码后,抛出的string类型的对象,最后只能用catch(…)来进行捕获,虽然用来语句来捕获异常,我们不知道哪里出错,但它保证了程序不会终止,也告诉了我们有人乱抛异常

  • 一般情况下抛出对象和catch是类型完全匹配的,若有多个类型匹配的catch,就选择最近的那个,但已有一些例外,允许非常量向常量的类型转换,也就是权限缩小,抛出string对象,用const string来捕获;允许数组转换称指向数组元素类型的指针,函数被转成指向函数的指针;允许从派生类向基类的转换,这个点非常实用,实际中继承体系基本都是用这个方法设计

允许从派生类向基类的转换,说明在继承体系中,抛出了一个派生类对象,可以用一个基类对象来接受。这样的设计,确保了在继承体系中,只要抛出的是派生类对象,我都可以用基类对象来进行捕获,若发现抛出的异常没有被我的基类对象捕获,那就说明有人没有按规则来抛出异常

来看下面的代码举例:

#include<thread>
class Exception //基类
{
public:
	//当抛出异常时,捕获错误信息和编号
	Exception(const string& errmsg,int errid)
		:_errmsg(errmsg)
		,_errid(errid)
	{ }

	virtual string what()const 
	{
		return _errmsg;
	}

	int Getid()const
	{
		return _errid;
	}
protected:
	string _errmsg;
	int _errid;
};

//不同的模块有不同的个性需求,就比如下面的派生类需要再捕获到一些其他错误的信息
class SqlException : public Exception 
{
public:
	SqlException(const string& errmsg,int errid,const string& Sql)
		:Exception(errmsg,errid)
		,_Sql(Sql)
	{ }

	virtual string what()const
	{
		string str = "SqlException:";
		str += _errmsg;
		str += "->";
		str += _Sql;
		return str;
	}
private:
	const string _Sql;
};

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

	virtual string what()
	{
		string str = "CacheExcepiton:";
		str += _errmsg;
		return str;
	}
};

class HttpException : public Exception
{
public:
	HttpException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id)
		, _type(type)
	{ }

	virtual string what()
	{
		string str = "HttpExcepion:";
		str += _type;
		str += ':';
		str += _errmsg;

		return str;
	}
protected:
	string _type;
};

void SqlMgr()
{
	if (rand() % 7 == 0)
	{
		//若随机值是5就抛出SQL的权限异常,并附上异常编号
		throw SqlException("权限不足", 101, "selecte * from name = 张三");
	}
	else
	{
		cout << "SqlMgr 调试成功" << endl;
	}
}

void CacheMgr()
{
	if (rand() % 5 == 0)
	{
		//若随机值是5就抛出cache的权限异常,并附上异常编号
		throw CacheExcepiton("权限不足", 101);
	}
	else if (rand() % 6 == 0)
	{
		//若随机值是6就抛出cache的资源异常,并附上异常编号
		throw CacheExcepiton("资源不存在", 100);
	}
	else
	{
		cout << "CacheMgr 调试成功" << endl;
	}
	SqlMgr();
}

void HttpServer()
{
	if (rand() % 3 == 0)
	{
		//若随机值是3就抛出HTTP的资源异常
		throw HttpException("资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0) 
	{
		//若是4就抛出HTTP的权限异常
		throw HttpException("权限不足", 101, "post");
	}
	else
	{
		//其他值就说明没有异常,正常走代码
		cout << "HttpServer 调试成功" << endl;
	}
	CacheMgr();
}
int main()
{
	srand((unsigned int)time(0));
	while (1)
	{
		this_thread::sleep_for(chrono::seconds(1));
		try
		{
			HttpServer();
		}
		catch (const Exception& e)//抛派生类,用基类捕获
		{
			//多态的调用
			cout << e.what() << endl;
		}
		catch(...)
		{
			cout << "Unknow Exception" << endl;
		}
	}
	return 0;
}

上述代码,实现了一个模拟程序,用随机值来进行模拟,因为现实当中,出现异常是偶然出现的。main函数中只捕基类和未知异常,代码中运用了经典的多态,基类就是捕获基本的异常,派生类则是在此基础上,根据自己的需求对基类进行重写。

运行的部分结果如下:

在这里插入图片描述
若没有抛基类或者派生类,就会用catch(…)来捕获

异常的重新抛出

有时catch捕获到一个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接throw,就可以把捕获的对象直接抛出。

举例:下面的代码是一个简单的线上聊天软件的项目

#include<iostream>
#include<string>
using namespace std;

class Exception
{
public:
	Exception(const string& errmsg,int errid)
		:_errmsg(errmsg)
		,_errid(errid)
	{ }

	virtual string what()const
	{
		return _errmsg;
	}

	int Getid()const
	{
		return _errid;
	}
protected:
	string _errmsg;
	int _errid;
};
class HttpException : public Exception
{
public:
	HttpException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id)
		, _type(type)
	{ }

	virtual string what()
	{
		string str = "HttpExcepion:";
		str += _type;
		str += ':';
		str += _errmsg;

		return str;
	}
protected:
	string _type;
};
void _sendmsg(const string& s)
{
	if (rand() % 2 == 0)
	{
		throw HttpException("网络不稳定,发送失败", 102, "put");
	}
	else if (rand() % 7 == 0)
	{
		throw HttpException("你已不是对方的好友,发送失败", 103, "put");
	}
	else
	{
		cout << "发送成功" << endl;
	}
}
void sendmsg(const string& s)
{
	//发送消息失败,则再重新发送三次
	for (size_t i = 0; i < 4; i++)
	{
		try
		{
			_sendmsg(s);
			break;
		}
		catch(const Exception& e)
		{
			//捕获异常,若错误编号是102,则网络不稳定重新发送
			//若错误编号不是102,则不是对方好友,异常重新抛出
			if (e.Getid() == 102)
			{
				//当i==3时,说明重新发送了三次还是失败,说明网络太差了,重新抛出异常
				if (i == 3)
				{
					throw;//异常重新抛出
				}
				cout << "开始第" << i + 1 << "重试" << endl;
			}
			else
			{
				throw;//直接抛出异常
			}
		}
	}
}

int main()
{
	srand((unsigned int)time(0));
	string str;
	while (cin >> str)//输入发送的信息
	{
		try
		{
			sendmsg(str);
		}
		catch (const Exception& e)
		{
			cout << e.what() << endl << endl;
		}
		catch (...)
		{
			cout << "Unknow Exception" << endl;
		}
	}
	return 0;
}

代码解析:在聊天时,消息发送不出去有很多种原因,可能是网络太差,又或者是对方已把你删除等等,上面的代码就是模拟对这两种原因进行处理。当需要发送信息时,如果网络正常且是对方好友,那么就可以正常发送,若是对方好友,但是网络太差,则会重新抛出一个异常提示网络不稳定,发送失败,但是我们知道当网络不稳定时,发送消息的文字前面是会有个灰色的圈圈在转的,那就说明消息还在尝试发送出去中,所以上面的代码也模拟实现了当第一次抛出网络不稳定的异常后,会被捕获,继续重新发送三次,若三次都发送失败,则会将异常重新抛出若不是对方好友,那么异常将会直接抛出

运行部分结果如下:
在这里插入图片描述
重新抛出异常,有时候不是为了对异常进行处理,可能是先捕获下来,对一些开辟的空间进行释放,或者特殊的情况先进行处理,处理完之后再重新抛出异常

异常安全问题

  • 我们知道当异常抛出后,后面的代码就不再执行,当我们申请了资源时,那么就会导致没有释放掉,这里就由于异常的原因导致内存泄漏,产生安全性的问题。这里我们就需要先将异常捕获下来,释放资源,再将异常重新抛出
  • 其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数需要释放10个资源,释放到第五个时需要抛出异常,则也需要捕获处理,不然就会内存泄漏

异常规范

对于用户和编译器而言,要是提前预知某个函数会不会抛异常,会有很多的好处,有助于简化调用函数的代码

  • 函数列表后面加上noexcept表示不会抛出异常,什么都不加表示可能会抛出异常,若一个声明了noexcept的函数抛出了异常,程序就会调用terminate终止程序
  • noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回false,不会则返回true

标准库的异常

C++标准库中也有一套自己的异常继承体系库,基类是exception,所以我们日常写程序,在主函数中,捕获exception即可,要获取异常信息,调用what函数,what是一个虚函数,派生类可以对其进行重写

在这里插入图片描述

对于异常的总结,就是捕获基类抛派生类

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值