C++异常

目录

异常的抛出和匹配原则:

异常安全:

异常规范

异常的优缺点


C语言处理错误的方式有两种:

1 终止程序:如assert,硬件异常收到信号等

这种处理方式的缺点就是直接终止程序用户无法接受。

2 返回错误码

这种处理方式的缺点就是需要程序员自己去查找错误码所对应的错误信息。

一般来说不是严重的错误基本都是返回错误码来处理,部分情况下使用终止程序来处理非常严重的错误。

而C++这种面向对象的语言的处理方式是异常。 当一个函数发现自己无法处理的错误是就可以抛出异常,让函数的直接或者间接调用者来处理这个错误。

异常的三个关键字:

throw:用户发现错误时抛出异常

catch:用于捕获异常

try:try中的代码块就是可能会抛出异常的代码,try和catch 配合使用,一个try可以配合多个catch

比如我们有一个除法的函数,如果除数为0就会发生除0错误,所以我们需要判断除数,当初数位0时不再进行运算而是抛出一个异常。

double division(double x, double y)
{
	if (y == 0)
		throw "除0错误";
	return x / y;
}

int main()
{
	try //代码块中是可能会抛异常的函数
	{
		double ret=division(5,0);
		cout << ret << endl;
	}
	catch (const char* err) 
	{
		cout << err << endl;
	}

正常情况 除数不为0的时候,我们的执行流是不会执行 catch 这个代码块的,只有在division中抛出异常了,然后才跳转到 catch 来执行,。

同时 division 抛出异常之后是不会再继续往后执行了,而是直接跳转到对应的catch语句中。

如果这个异常我们不去捕获的话,程序就会终止

如果有多个catch,那么执行流会跳转到哪一个catch呢?当然是最近且参数最匹配的catch。如果我们的参数都不匹配,那么也是相当于未捕获这个异常,还是会终止程序。

异常的抛出和匹配原则:

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

throw抛出的是对象,可以是自定义类型,也可以是内置类型,取决于你怎么描述这个错误。

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

调用链就是函数的调用关系,比如我们在上面的代码的基础上再来一层函数        

double division(double x, double y)
{
	if (y == 0)
		throw "除0错误";
	return x / y;
}

double func(double x, double y)
{
	try
	{
		double ret = division(x,y);
		return ret;
	}
	catch (const char*err)
	{
		cout <<"func:" << err << endl;
	}
}

int main()
{
	try //代码块中是可能会抛异常的函数
	{
		double ret=func(5,0);
		cout << ret << endl;
	}
	catch (const char* err) 
	{
		cout<<"main" << err << endl;
	}

	return 0;
}

抛出异常时,是从当前栈帧开始,逐层找参数匹配的catch代码块。如果当前栈帧不匹配,那么就会先销毁当前栈帧,再到上一层栈帧去找。

当然,还有可能throw本身就是在一个try catch 代码块中,也会看参数是否匹配。

在当前函数抛出,当前函数捕获是允许的。

当匹配到main栈帧之后发现还是没有匹配的catch,那么程序就会退出。

如果找到了匹配的catch,就会在处理完匹配的catch的语句之后,在catch后继续执行。

3 抛出异常对象之后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象在被catch捕获之后就销毁。

类似于传值返回,如果异常对象的类实现了移动构造,那么就是用过移动构造来捕获,如果没有实现,那么就还需要一次深拷贝。在这个临时对象不断向上传递的时候,是以右值的形式传递的。

4 catch(...) 可以用来捕获任意类型的异常。

这个就好比我们的switch case语句中的default 。当出现了我们事先不知道的异常类型之后,如果我们没有catch(...),就会导致异常没有被捕获而程序退出。 

所以我们一般try catch 最后会写一个catch用来捕获未知异常。 但是由于不知道捕获到的异常的类型是什么,所以我们也无法对其进行解析。

在异常这里其实是有很多的坑等着我们去跳的,首先,就是由于抛出异常之后,执行流就会直接跳转到catch语句中,这种跳转有时候是会出问题的,比如下面这种情况,我们在throw之前申请了堆空间:

void func(int x)
{
	int* pa = new int;
	if (x == 0)
		throw "error";

	delete pa;
}

int main()
{
	try
	{
		func(0);
	}
	catch (const char*err)
	{
		cout << err << endl;
	}
	catch(...){}
	return 0;
}

这时候就会出现,堆空间未释放销毁函数栈帧跳转到别的栈帧的情况,也就是内存泄漏。

我们也把这种情况称作一场安全问题,要怎么解决呢?

最简单的法子就是直接在抛出异常的地方进行捕获,捕获的时候顺便就把堆空间释放掉。

void func(int x)
{
	int* pa = new int;
	try
	{
		if (x == 0)
			throw "error";
	}
	catch (const char* err)
	{
		delete pa;
		cout << err << endl;
	}
	catch (...) 
	{ 
		delete pa;
	}
	delete pa;
}

但是这种办法有很多的弊端,

1 如果我们的代码块中会抛出多种异常,那么就需要多个catch来捕获,那么就需要在每一个catch中都写delete的代码,代码冗余。

2 在有些场景下,我们是需要在main函数中统一捕获异常进行处理,用来记录日志的。

第一个问题我们目前不好解决,最好的办法就是使用智能指针或者其他的RAII风格的类进行管理这块堆空间。

而第二个问题的解决办法就很简单了,我们在当前栈帧捕获之后,可以不在此处进行处理,而是选择继续抛出异常,将异常继续抛给调用链的上层的函数栈帧。、

这就叫做异常的重新抛出

	catch (const char* err)
	{
		delete pa;
		//cout << err << endl;
		throw err;
	}
	catch (...) 
	{ 
		delete pa;
	}

但是对于这种 ... ,我们不确定的类型怎么重新抛出呢?

其实C++给我们提供了一种重新抛出异常的简单方式,就是直接使用 throw ,throw会将 catch捕获的异常直接重新抛出,这时候就不需要我们知道类型了。

同时,像上面这种我们捕获异常只是为了暂时拦截执行流来做一些释放资源之类的事情,我们并不需要具体部或某种类型的异常,可以直接catch(...),然后干完该干的事之后继续向上抛出异常,因为我们不在这一层栈帧中进行异常的处理。

void func(int x)
{
	int* pa = new int;
	try
	{
		if (x == 0)
			throw "error";
	}
	catch (...) //单纯拦截执行流,不对异常做处理。
	{
		delete pa;
		throw;
	}
	delete pa;
}

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

比如C++官方库中提供了异常的基类,同时C++库中的异常也都是继承自该类

但是由于官方的这个exception不太行或者说太复杂,所以我们一般也不用官方的这个异常基类。

那么,如果我们在一个try catch块中,既有捕获子类的catch,也有捕获父类的catch ,会进哪个呢?

首先,使用基类捕获子类是异常的匹配原则中天然就能匹配的,那么检测的时候,如果抛出的是子类对象,那么进基类的catch和进子类的catch都是不需要任何转化的。那么按照匹配原则的第二点,会被位置更近的catch所捕获。

当然,在实际中我们只需要捕获基类就行了,除非你对某一个子类要进行特殊处理,那么可以放在基类的catch前先捕获处理。

异常安全:

1 不要在构造函数抛出异常,否则可能会导致对象不完整或者初始化不完全

2 不要在析构函数中抛出异常,否则可能导致内存泄漏

3 注意在局部域申请的堆空间要记得释放,以及加的锁要注意解锁。

异常规范

1 异常规范说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。可以在函数的声明后面加上 throw(可能抛出的异常类型)。

void func(int x) throw(int,const char*)
{
	if (x == 0)
		throw 3;

	if (x == 1)
		throw "111";	
}

如果抛了一些不在该范围内的类型的异常,编译的时候也会有所提示

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

void func() throw()   //括号中不写类型标识不抛异常
{
}

如果你写了throw() ,然后在函数体中回抛异常的话,那么编译的时候会提示

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

这也就是我们以前的写法。

但是C++的异常规范有一些问题:

1 过于复杂。因为我们在函数中可能调用库函数,库函数也是有可能会抛异常的,难道我们还要知道每一个库函数可能抛出的异常类型或者每次使用都要去查文档? 这太麻烦了,所以我们一般都不采用这种方式。

2 没有在语法上强制规定。 

这就导致了,有的使用者并不会按照这个异常规范写函数,那么其他的用户想要调用这些函数也不知道该函数是否抛异常,那么自然也就扯淡了。

于是C++就加了一个关键字来简化了异常规范,就是 noexcept  ,一个函数在后面如果加了 noexcept,那么就说明该函数不会抛异常,否则可能抛出任意类型的异常。

那么使用noexcept就简单了很多了,如果确定不会抛异常我们就可以加上该关键字。

异常的优缺点

优点:

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

2 返回错误码的传统方式有一个很大的问题就是,需要层层返回。 比如我们需要在 main 函数中处理错误,记录日志等,而如果调用链的深层的函数出现了错误,如果是返回错误码的方式,则需要层层返回,很复杂,而如果是异常,则可以直接跳转到catch。

3 很多的第三方库都包含异常,比如boost,gtest,gmock等,那么我们也要知道异常的知识 

4 部分函数适合使用异常来处理。比如构造函数这种没有返回值的(比如函数中调用了new,new失败了会抛异常),又或者是类似于方括号重载这种函数,他的返回值是数据,那么就不能返回错误码了,要么就终止程序,要么就抛异常。

缺点:

1 异常会导致执行流乱跳,并且非常的混乱,而且是运行时出错才会跳转,在编译时无法看出来,这会导致我们调试困难。因为我们在程序中打的断点可能会被跳过不执行。 

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

3 C++没有资源回收机制,资源需要自己管理,那么有了异常之后,就很容易出现资源未释放或者锁没有解锁等。 解决的方案就是使用RAII的方式来管理资源,学习成本较高。

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

5 异常尽量规范使用,否则后果不堪设想,如果随意抛异常,外层捕获异常的用户就会苦不堪言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值