【C++】异常

目录

一、异常的概念

二、异常的抛出与捕获

三、异常的重新抛出

四、抛出异常的风险

五、抛出异常的规范

六、C++标准库中的异常体系

七、异常的优缺点


一、异常的概念

在C语言中没有异常的概念,我们一般通过以下方式来处理错误

  • 使用assert断言等提前检查的方式,一旦发生错误直接终止程序
  • 返回错误码

在C++中,我们还可以通过抛出异常的方式来处理程序中出现错误的情况

异常是一种处理错误的方式,抛出异常的目的是让函数的直接或间接调用者来处理这个错误

例如在某个函数中出现了除零错误野指针等错误,我们可以抛出异常,并在调用该函数的地方捕获这个异常

这里介绍三个关键字,用于抛出异常和捕获异常

  • throw:当出现错误时,我们可以通过throw关键字抛出一个异常
  • try:通常在try代码块内部调用可能会抛出异常的函数或代码块,try后面通常跟着一个或多个catch块用于捕获异常
  • catch:try块内部的函数抛出异常后,由catch关键字捕获该异常,并执行catch块内部的代码

即使用throw抛出异常,使用try和catch捕获异常。try块中的代码称为保护代码

我们来看一个抛出和捕获异常的例子:

int func(int a, int b)
{
	if (b == 0)
		throw "Division by zero condition!"; //除零错误
	else
		return a / b;
}

int main()
{
	try {
		func(1, 0); //保护代码
	}
	catch (const char* errmsg) //对应抛出异常的类型
	{
		cout << "捕获异常:" << errmsg << endl;
	}
	catch (...) //捕获任意类型的异常
	{
		cout << "捕获未知异常" << endl;
	}
	return 0;
}

结果:

【C++】异常_第1张图片


二、异常的抛出与捕获

通过上面的例子可以看出,抛出异常实际上就是抛出不同类型的对象

异常的抛出与捕获有以下几个原则:

  • 通过抛出的对象类型决定激活哪个catch块的处理代码
  • 函数间存在调用链,即一个函数调用另一个函数。若调用较深的函数抛出异常,异常会被层层递出直到遇到离抛出函数最近的一个类型匹配的catch块
  • 如上,catch(...)可以捕获任意类型的异常
  • 例外的,抛出一个子类对象,可以使用父类来捕获
  • 若抛出的对象是临时对象,会生成一个拷贝对象直到被catch后才销毁

关于调用链,我们知道在一个函数中调用另一个函数实际上就是建立新的栈帧

如果在位于这个栈结构上层的函数中抛出了一个异常,那么首先会在抛出异常的位置检测是否存在try块,如果存在则查找匹配的catch语句

如果不存在,则顺着栈结构向下查找其他函数中是否存在匹配的catch语句,就像是剥洋葱一样一层层查找。如果一直到main函数中都没有找到匹配的,则直接终止程序

这样沿着调用链查找匹配的catch语句的过程叫做栈展开


三、异常的重新抛出

既然存在栈展开,那么一定可以捕获异常后重新抛出异常

有时,单个catch不能完全处理一个异常,我们进行一些校正处理后可以重新抛出异常,交给更外层的函数进行二次或多次处理

例如在函数调用链中间可能存在一些new出来的对象,如果深层的函数抛出了异常,中间需要先捕获异常并释放new出来的空间,再选择把异常抛出给外层函数处理,以避免出现内存泄漏

int Div(int a, int b)
{
	if (b == 0)
		throw "Division by zero condition!"; //除零错误
	else
		return a / b;
}

void Func()
{
	int* arr = new int[10];
	try {
		cout << Div(1, 0) << endl;
	}
	catch (...)
	{
		cout << "delete[]" << endl;
		delete[] arr; //完成内存释放
		throw; //重新抛出异常,不加对象则捕获到什么就抛出什么
	}
	delete[] arr; //如果在调用Div时出现异常,则不会执行这里的delete[]
}

int main()
{
	try {
		Func();
	}
	catch (const char* errmsg)
	{
		cout << "捕获异常:" << errmsg << endl;
	}
	catch (...)
	{
		cout << "捕获未知异常" << endl;
	}
	return 0;
}

【C++】异常_第2张图片

可以看到,Func函数中申请的内存空间也成功释放了,没有造成内存泄漏


四、抛出异常的风险

前面提到的内存泄漏也是抛出异常的风险之一,由于抛出异常导致执行流意外跳出函数而导致申请的空间没有被释放

  • 在构造函数内抛出异常,可能导致对象不完整或没有完全初始化
  • 在析构函数内抛出异常,可能导致资源泄漏
  • 在lock和unlock之间抛出异常导致死锁

针对这类问题,常用的解决思想是RAII(Resource Acquisition Is Initialization),简单来说就是利用类对象出作用域会自动析构的特点来自动释放资源,这一点在后面的智能指针部分会讲到


五、抛出异常的规范

  • 在函数名后面接throw(类型),用来说明该函数可能会抛出的所有异常类型

例如:

void func() throw(const char*, int, double, float)
{
	//...
}
  • 函数名后面接throw(),括号内不填类型,用来说明该函数不会抛出异常
  • C++11中新增的noexcept也表示某个函数不会抛出异常
void func() noexcept;

实际上,就算我们抛出了没有括号中说明的异常也不会报错,因为这只是一个规范而不是强制要求,就像我们前面也没有在函数名后面加throw(类型)

不过加上noexcept就是真的不能抛出异常了


六、C++标准库中的异常体系

C++标准库中也定义了一系列异常,我们可以在程序中捕获这些异常,其中exception是所有这些标准异常的父类

【C++】异常_第3张图片

这些异常的具体说明:

异常 描述
std::exception 该异常是所有标准 C++ 异常的父类
std::bad_alloc 该异常可以通过 new 抛出
std::bad_cast 该异常可以通过 dynamic_cast 抛出
std::bad_typeid 该异常可以通过 typeid 抛出
std::bad_exception 这在处理 C++ 程序中无法预期的异常时非常有用
std::logic_error 理论上可以通过读取代码来检测到的异常
std::domain_error 当使用了一个无效的数学域时,会抛出该异常
std::invalid_argument 当使用了无效的参数时,会抛出该异常
std::length_error 当创建了太长的 std::string 时,会抛出该异常
std::out_of_range 该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator[]()
std::runtime_error 理论上不可以通过读取代码来检测到的异常
std::overflow_error 当发生数学上溢时,会抛出该异常
std::range_error 当尝试存储超出范围的值时,会抛出该异常
std::underflow_error 当发生数学下溢时,会抛出该异常

在这些异常类中还提供了方法what(),可以返回异常产生的原因,例如:

【C++】异常_第4张图片

可以看到,这些异常都是exception类的子类,我们自然也可以通过继承exception类来实现自己的异常类

除此之外,我们也可以自定义一套自己的基于继承的异常体系,这样只需要捕获一个基类就可以捕获所有抛出的异常了


七、异常的优缺点

异常的优点:

  1. 异常相比错误码能够更加清晰准确的展示错误的信息等内容,有助于更好的定位程序的bug
  2. 相比错误码只能在最外层获取错误信息,异常可以在调用链的任何环节进行错误处理
  3. 在一些没有返回值或返回值不为int的函数中无法很好的使用错误码返回错误信息,用异常更好处理

异常的缺点:

  1. 抛出异常会导致程序执行流乱跳,导致跟踪调试和分析程序时比较困难
  2. 抛出异常可能导致内存泄漏、死锁等安全问题,需要妥善处理资源的管理问题
  3. C++标准库的异常体系定义的不好,导致大家都倾向于各自设计属于自己的异常体系
  4. 如不规范使用,随意抛出异常,会导致外层用户捕获异常十分困难

总体而言异常还是利大于弊,我们使用时注意遵守规范即可

完.

你可能感兴趣的:(C++,c++)