目录
一、异常的概念
二、异常的抛出与捕获
三、异常的重新抛出
四、抛出异常的风险
五、抛出异常的规范
六、C++标准库中的异常体系
七、异常的优缺点
在C语言中没有异常的概念,我们一般通过以下方式来处理错误
在C++中,我们还可以通过抛出异常的方式来处理程序中出现错误的情况
异常是一种处理错误的方式,抛出异常的目的是让函数的直接或间接调用者来处理这个错误
例如在某个函数中出现了除零错误或野指针等错误,我们可以抛出异常,并在调用该函数的地方捕获这个异常
这里介绍三个关键字,用于抛出异常和捕获异常
即使用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;
}
结果:
通过上面的例子可以看出,抛出异常实际上就是抛出不同类型的对象
异常的抛出与捕获有以下几个原则:
关于调用链,我们知道在一个函数中调用另一个函数实际上就是建立新的栈帧
如果在位于这个栈结构上层的函数中抛出了一个异常,那么首先会在抛出异常的位置检测是否存在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;
}
可以看到,Func函数中申请的内存空间也成功释放了,没有造成内存泄漏
前面提到的内存泄漏也是抛出异常的风险之一,由于抛出异常导致执行流意外跳出函数而导致申请的空间没有被释放
针对这类问题,常用的解决思想是RAII(Resource Acquisition Is Initialization),简单来说就是利用类对象出作用域会自动析构的特点来自动释放资源,这一点在后面的智能指针部分会讲到
例如:
void func() throw(const char*, int, double, float)
{
//...
}
void func() noexcept;
实际上,就算我们抛出了没有括号中说明的异常也不会报错,因为这只是一个规范而不是强制要求,就像我们前面也没有在函数名后面加throw(类型)
不过加上noexcept就是真的不能抛出异常了
C++标准库中也定义了一系列异常,我们可以在程序中捕获这些异常,其中exception是所有这些标准异常的父类
这些异常的具体说明:
异常 | 描述 |
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(),可以返回异常产生的原因,例如:
可以看到,这些异常都是exception类的子类,我们自然也可以通过继承exception类来实现自己的异常类
除此之外,我们也可以自定义一套自己的基于继承的异常体系,这样只需要捕获一个基类就可以捕获所有抛出的异常了
异常的优点:
异常的缺点:
总体而言异常还是利大于弊,我们使用时注意遵守规范即可
完.