【C++】异常 详解

目录

异常的引出与简介

异常的使用

异常逻辑图解

异常继承体系

异常的重新抛出

异常安全

异常规范

结语


异常的引出与简介

我们可以回忆一下,在C语言时期,我们返回错误的方式只有两个

一个是assert强制返回错误,还有一个就是返回错误码errno

我们来看一段代码:

// C语言的报错

int main()
{
	FILE* fout = fopen("Test.txt", "r");
	cout << fout << endl;
	cout << errno << endl;
	perror("open fail");
	return 0;
}

注意,C++兼容C语言,我们这里只是想看看两个语言之间返回错误的区别

我们会看到,C语言用到的主要就是errno和perror

但是,有时这样返回的信息并不明确,而且,我们如果每一个都要这样写的话,代码会变得非常冗余,所以在C++就变了一种方式返回错误——异常

我们C++的异常主要有try、catch、throw三个

我们会在try这里进入某段程序,然后try后面是跟着catch的,如果我们在try调用的程序里面有throw(抛出错误)的话,那错误就会被catch捕捉到

使用try、catch语法的语句如下:

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

注意,上述代码中的ExceptionName 也可以理解为异常的类型

比如,我throw的是一个string类型的异常,那我这里就用string类型来接收

异常的使用

首先,我们的异常返回时,会被距离最近且符合条件的catch捕获

然后,我们的异常在抛出的时候,其实是抛出了一个异常的拷贝,比如我抛出了一个string,但其实我抛出的是那个string的拷贝,但是这时就可以用到我们右值引用的知识了,这里也可以走一个右值引用或者看编译器的优化

我们先来看一段代码,顺着代码学习:

int main()
{
	try
	{
		func();
	}
	catch (const string& s)
	{
		cout << s << endl;
	}

	return 0;
}

首先,我们先在try里面写了一个func函数,然后外面用一个string类型catch

那我们再去看看func:

void func()
{
	int a, b;
	cin >> a >> b;

	try
	{
		cout << Division(a, b) << endl;
	}
	catch(const char* s)
	{
		cout << s << endl;
	}
	catch (int sz)
	{
		cout << sz << endl;
	}

}

可以看到,我们在func里面需要cin两个值,然后再try一个函数,最后外面用两个catch接收

再来看看这个Division函数:

int Division(int a, int b)
{
	if (b == 0)
	{
		string s("Division by 0");
		throw s;
	}
	else
	{
		return (a / b);
	}
}

我们的Division里面是——当我们传过来的参数 b是 0 的时候,我们就抛出一个类型为string的异常

如果不是,就返回两个参数相除之后的结果

首先我们试试会抛异常的情况

cin完两个值之后,会再去执行Division

但由于第二个参数为0,所以就 throw 异常

但是我们会发现,他直接匹配到了最外面的catch了(有看不懂的没关系,待会儿会讲)

这时因为我们func里面的两个catch,一个是char*,一个是int,都不是string,而这里会严格匹配,并不会类型转换

然后我们就来讲讲 catch(...)

我们试想一下,如果抛出异常的人随便抛异常的话,那么在外面的接收是不是就有可能造成接收不到异常啊

所以为了防止接收不到,我们就有了 catch(...) 

这个 catch(...)  的意思就是,只要你是抛出的异常,不管你是什么类型,我这里都能接收

我们可以做一个实验,Division里面抛出的异常还是string类型,但是我们在main函数里面将string类型的catch给去掉,看看这个 catch(...)  会不会接收:

int main()
{
	try
	{
		func();
	}
	catch (...)
	{
		cout << "unknown exception" << endl;
	}

	return 0;
}

我们会看到,就匹配到了 catch(...) 这里来了

如果我们没有合适的catch接收,又没有 catch(...) ,那么就会报错

int main()
{
	try
	{
		func();
	}
	catch (int i)
	{
		cout << "unknown exception" << endl;
	}

	return 0;
}

抛出的是一个string,但是我们这里没有捕获到

另外还有一个点,如果我们中途抛出的异常,那么下面的代码就不会执行,但是如果我们是捕获异常的话,那么接下来的代码就还是会执行的

int Division(int a, int b)
{
	if (b == 0)
	{
		string s("Division by 0");
		throw s;
	}
	else
	{
		return (a / b);
	}
	cout << "xxxxxxxxxxxxxxxxxxxxx" << endl;
}

比如我们看这个Divison,如果我们在前面直接就throw了的话,我们后续的xxxxxxxxx就不会打印

但如果是catch之后的程序,那么就会执行

void func()
{
	int a, b;
	cin >> a >> b;

	try
	{
		cout << Division(a, b) << endl;
	}
	catch(const char* s)
	{
		cout << s << endl;
	}
	catch (string sz)
	{
		cout << sz << endl;
	}

	cout << "xxxxxxxxxxxxxxxxxxxxx" << endl;

}

比如我们看,Divison那里会抛出一个string的异常,这次我们在func里面就catch

那么我们catch之后,xxxxxxxxxx还是会打印的

还有就是,我们的程序一般都不会只执行一次

举个例子:我们的一些app有时候会发生一些错误,比如说突然卡了,游戏出bug了,这些都是发生错误了,难道说,我们游戏玩着玩着卡了一下,发现了一些bug,比如我突然发现卡在某个游戏的墙里就不会被达到,那是你一发现就直接游戏退出吗?

还有,我们的电脑下面都会一直开着一些程序:

如果我们微信消息突然发不出去了,就会直接退出微信,那是不是有点离谱啊

所以一般情况下,我们的程序都是会长期运行的,然后他会监视你的行为,比如:如果你按了退出键,我们才会结束程序

综上,我们将我们的程序改一改,可以直接写成一个while循环,就先写死吧,这里只做演示

int Division(int a, int b)
{
	if (b == 0)
	{
		string s("Division by 0");
		throw s;
	}
	else
	{
		return (a / b);
	}
}

void func()
{
	int a, b;
	cin >> a >> b;

	try
	{
		cout << Division(a, b) << endl;
	}
	catch(const char* s)
	{
		cout << s << endl;
	}
	catch (int sz)
	{
		cout << sz << endl;
	}

	cout << "xxxxxxxxxxxxxxxxxxxxx" << endl;

}

int main()
{
	while (1)
	{
		try
		{
			func();
		}
		catch (const string& s)
		{
			cout << s << endl;
		}
		catch (...)
		{
			cout << "unknown exception" << endl;
		}
	}

	return 0;
}

主要是看main函数,我们写的一个while循环

异常逻辑图解

异常继承体系

我们先来听一个小故事:

首先一个实习生,我们叫他小叶,然后有一个职场老手,我们叫他大黄

在某一天,这两个人因为哪个足球队会赢吵起来了,一个觉得法国会赢,一个觉得阿根廷会赢

这时候大黄超不过小叶,就寻思着怎么报复

所以大黄就悄悄用手机录音,然后跟小叶说他要抛某个错误,但是这会儿小叶气头上呢,也没听见

后面,不出意外地有未知异常被 catch(...) 捕捉到了,然后一查发现是大黄和小叶之间的问题

然后小叶就说是大黄抛了新异常没告诉他

这时候大黄就拿出了录音,反咬一口说已经和小叶说了但是他天天看那个球队不好好工作

最后小叶就被罚了

晚上回家之后,小叶痛定思痛,通宵翻书看异常那里的问题,结果发现:

我们可以写一个父类,然后让不同的子类继承这个父类,最后要抛异常就直接抛子类,在父类里面写一个虚函数,而子类再重写,就实现了多态,这样就可以实现抛出不同异常的效果了

然后,小叶第二天就跑到部门那里跟领导说可以这样改,这领导一听,这小叶可以啊,刚来没多久就对技术方案提出改进,然后就月末的奖金就给了多一些

这件事情告诉我们,以后害人之前记得录音

哦不不,是技术才是王道

接着我们再来看看上面提到的异常继承

我们先来看一个父类:

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;
};

首先这是一个父类

我们在这个父类里面写了一个虚函数what和一个getid,这个getid后面的重试部分才会有所涉猎

我们的这个父类里面有两个成员,一个string,一个int,这时我们的what函数就是返回这个string

再来看几个继承下去的子类:

class SqlException : public Exception
{
public:
	SqlException(const string& errmsg, int id, const string& sql)
		:Exception(errmsg, id)
		, _sql(sql)
	{}

	virtual string what()
	{
		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()
{
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}

	cout << "调用成功" << endl;
}

void CacheMgr()
{
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}
	SQLMgr();
}

void HttpServer()
{
	if (rand() % 3 == 0)
	{
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}

	CacheMgr();
}

int main()
{
	srand(time(0));

	while (1)
	{
		// 线程相关的知识,意思是休息一秒再执行
		this_thread::sleep_for(chrono::seconds(1));

		try
		{
			HttpServer();
		}
		catch (const Exception& e) // 这里捕获父类对象就可以
		{
			 多态
			cout << e.what() << endl << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}

	return 0;
}

上述代码中,我们写了三个函数,然后在一个函数的末尾再去调用另一个函数以此将每个函数都串起来

然后我们的main函数里卖弄写了一个while(1)死循环代表会一直执行下去

我们的每个函数都会抛出不同的异常,抛出的都是不同的子类,而每个子类里面又重写了虚函数

总结一下:

异常继承体系就是,写一个父类(有虚函数),然后子类继承之后重写虚函数构成多态,重写的虚函数的内容就是要返回的异常的内容,最后抛异常就抛出子类,用父类的指针或引用catch,就构成了多态

异常的重新抛出

现实生活中,我们会遇到这样的事情:由于网络不好,所以消息会不断重新发送

这就是异常重新抛出的一个体现

如果我们的消息是因为网络不好而发不出去的话,就直接说发不出去,是不是有点不妥啊

所以,就有一个重新发送的机制

但如果是,昨天你和你兄弟出去见面,但是你兄弟把他的一个女同学一起叫出来了,你还和人家加了微信

然后今天你敲完了代码之后,想着约人家女孩子出来晚上一起看个电影,结果发现,下面有一个红色小感叹号,说你已不是对方的好友

这种情况也是错误,但是我们就不需要重试

所以在这里,我们就需要用到我们父类里面的 int _id 了

我们举个例子:

假设我们写一个for循环将tey、catch都包住

然后写一个函数来表达发消息的行为

但是我们怎么知道,throw回来的异常,是因为网络不好要重试,还是因为不是好友了发不出去

这时候我们就需要用到错误码了,根据不同的错误码我们就能判断出哪个异常,进而选择重试与否

void HttpServer()
{
	///
	// 失败以后,再重试3次
	// 需要先执行依次,然后才是重复三次
	for (int i = 0; i < 4; i++)
	{
		try
		{
			sendmsg("想看电影 想找个人陪 一起看 有空否");
			break;
		}
		catch (const Exception& e)
		{
			if (e.getid() == 102)
			{
				// 进入重试环节
				if (i == 3)
					throw HttpServerException("网络不稳定,发送失败", 102, "put");
				cout << "开始第" << i + 1 << "重试" << endl;
			}
			else
			{
				throw HttpServerException("你已经不是对象的好友,发送失败", 103, "put");
			}
		}
	}

	///
}

接着我们再来写一写sendmsg函数

但是这里我们测试一下就可以了

void sendmsg(const string& s)
{
	if (rand() % 2 == 0)
	{
		throw HttpServerException("网络不稳定,发送失败", 102, "put");
	}
	else if (rand() % 3 == 0)
	{
		throw HttpServerException("你已经不是对象的好友,发送失败", 103, "put");
	}
	else
	{
		cout << "发送成功" << endl;
	}
}

综上,我们的重写这里主要想讲的就两个点:

  1. 错误码的作用就是:可以根据特定的错误决定下一步行动
  2. 不是所有的程序都是抛完异常就结束了,有可能还会有重新执行的操作

异常安全

其实异常也有隐患在,试想一下,我们的异常是:throw之后的代码不会执行

但要是如果我在throw之前,我就开辟了空间,但是我throw之后的代码就有销毁空间的操作,但是我throw跳过了

这时候就会发生经典的内存泄漏错误

这是一个大问题

还有就是,假如我们在下面的程序里有抛出异常,都没有错

但是我前面那里,我开了空间,用了new

如果是我都new抛异常呢?那我们是不是也得对new进行一系列操作啊

这样太麻烦了

这些问题最后,都会由一个叫做智能指针的东西来解决,会在后面学到

异常规范

经过上面的简介,我们现在开始有点害怕了,因为我们不能随便写函数了

因为如果我们写了这个函数,在这之前又开辟了空间,但是如果这个函数会抛异常,那后面的代码就会因为没有调用到而导致内存泄漏

那么我们就有了一个异常规范的概念:

如果这个函数不会抛异常,就在这个函数后面加上 noexcept

但是这其实不是强制要求的,所以就会有人乱用,或者其实不加也可以。。。

但是加上了之后,如果这个函数会抛异常但是你却将noexcept加上了,那么代码不会报错只会报警高,但是你代码跑起来的时候就会跑不过

因为加上之后编译器默认你不会抛异常,所以就导致了报错

结语

看到这里,这篇博客有关异常的相关内容就讲完啦~( ̄▽ ̄)~*

如果觉得对你有帮助的话,希望可以多多支持博主喔(○` 3′○)

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值