目录
1、异常简介
(1) 异常的概念
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。C++异常通常涉及到throw、try、catch 关键字。
- throw 关键字:抛出关键字
- try 关键字:划定异常的检查范围
- catch 关键字:负责捕获 try 块内抛出的异常。“ ... ” 代表任意异常。
void func1() {
throw "An Exception Is Throwed"; // 抛出一个异常
}
void func2() {
try // 检查当前区块中是否有异常抛出
{
func1();
}
catch (const char* estr) // 捕获try区块中的异常
{
cout << estr << endl;
}
catch (...) { // ... 代表任意异常
cout << "Unknow Exception" << endl;
}
}
注意:最后放catch(...) 的原因是,如果异常没有被捕获,程序会直接终止,没有被捕获的原因是抛出异常的类型和所有catch语句捕获的异常类型不一致。
(2) 异常的匹配流程
当一个异常被抛出以后,异常会先检查自己是否处在try关键字的检查范围内
- 如果不在try 关键字的检查范围内,那就往上一级函数栈查找;
- 如果在try 关键字的检查范围内,那就查找和异常类型对应的catch语句
这个过程会一直持续,直到main函数栈帧 或者 找到匹配的catch语句。如果异常被catch语句捕获了,会继续沿着catch子句后面继续执行;如果到达main函数的栈帧,依然没有找到和异常类型匹配的catch语句,程序会直接终止。
注意:func1()后面的 cout 语句不会被执行。一旦发现自己处在try语句的区块以后,会直接跳到catch语句一个个匹配,不会运行try区块中的其他内容。
2、异常安全
异常可以准确地展示各种错误信息,但是也存在一定缺陷,从上面我们知道,异常确认自己处在try区块以后,会直接跳到catch语句进行匹配,不会执行try区块中的其他语句。
以下面这个图为例,func2() 先动态开辟了一块空间,此时抛出了异常,然后略过delete语句 直接跳转到了catch语句。很显然 动态开辟的空间 p 没有被释放,此时造成了内存泄漏。这是异常安全的一种情况。
(1) 异常安全的几种情况
第一种,构造函数中抛出异常。构造函数负责完成对象的构造和初始化,初始化过程中抛出异常,可能导致对象不完整或没有 完全初始化。
第二种,析构函数中抛出异常。析构函数负责主要完成资源的清理,析构过程中抛出异常可能会导致内存泄漏(动态空间未被释放、句柄未被关闭等)
第三种,new 和 delete、lock 和 unlock 之间抛出异常。不加处理会直接导致内存泄漏(详见上图),后续智能指针会解决这个问题。
(2) new和delete之间抛出异常的基本处理思路
基本思路就是将动态开辟的资源交由一个类SmartPtr来管理,出了try的作用域以后,该类的对象就会被销毁,此时就会调用该类的析构函数,我们可以在析构函数中释放空间。
资源管理类的定义如下:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr) :_ptr(ptr) {}
~SmartPtr() {
cout << "delete: " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
private:
T* _ptr;
};
然后我们修改一下 try 区块中的代码逻辑,sp对象处在try区块的作用域,一旦出了该作用域就会调用SmartPtr 的析构函数。
void func1() {
throw "An Exception Is Throwed";
}
void func2() {
try
{
int* p = new int;
SmartPtr<int> sp(p); // 交由SmartPtr管理(出了try区块的作用域就会调用析构函数)
func1();
}
catch (const char* estr)
{
cout << estr << endl;
}
catch (...) {
cout << "Unknow Exception" << endl;
}
}
测试结果如下,我们发现,资源被释放了,异常也被捕获到了,其实这就是智能指针最基本的原理,借由析构函数来控制资源的释放。