1.C语言对于异常的处理:
- 方法一:在之前我们遇到一些bug的时候,通常会用
if
判断或者assert
断言等问题进行处理。但这种方式太过暴力,会直接中断程序的运行- 方法二:返回错误码,C语言的报错大多使用这种方式。不过这需要程序的用户自己去查对应的错误码表格,较为麻烦
2.C++对于异常的处理:
- C++标准库中便使用了一个
exception
类来进行异常的处理,我们运行程序中遇到的一些报错,其实就是标准库里面抛出了对应的异常其操作主要借助下面三个关键字:
- throw:在出现问题的地方抛出异常(throw关键字可以抛出
任意类型
的异常)- try:监控后续代码中出现的异常,后续需要以catch作为结尾
- catch:用于捕获异常,同一个try可以用多个不同类型的catch进行捕获
int Div() { int a, b; cin >> a >> b; if (b == 0) throw "div 0 err!"; return a / b; } int main() { try { cout << Div() << endl; } catch(const char* s) { cout << s << endl; } return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
我们在进行异常处理的时候需要注意三点,否则容易出问题:
catch类型对应
当我们进行抛异常的时候,一定需要有对应类型的catch,否则会报错
比如我们
throw
的是一个常量字符串
,如果用string
来catch,就会因为类型不匹配而出现报错
- 所以当我们使用某一个会抛异常的函数的时候,一定要注意其抛出异常的类型
利用…进行全捕获
假设我们不知道这里面会抛出什么类型的错误呢?总不能把所有类型都catch一下吧?
当然不需要,我们可以使用下面的函数进行全捕获
- 这就可以用于当我们不知道报错类型的时候。不过一般的使用场景是,在这之前先
catch
已知的错误类型,最后再加上一个全捕或,作为未知错误
的标识
- 不过
catch(...)
有一个缺点,那便是我们不能知道异常的类型
基类捕获派生类的异常
当我们出现异常的时候,如果
throw
了一个子类对象,可以用基类的引用来接收!这个在进行继承多态的错误编写的时候就很有用
class A { int a; }; class B : public A { int b; public: B() :b(1) {} }; void testab() { B bt; throw bt; } int main() { try { testab(); } catch (A& e) { cout << "err class A" << endl; } catch (...) { cout << "err" << endl; } return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
异常抛出规则:
- 异常时通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码
- 被选中的处理代码的调用链是,找到于该类型匹配且离抛出异常位置最近的那一个catch
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象
- catch(…)可以捕获任意类型的对象,主要是用来捕获没有显示捕获类型的异常,因为如果没有匹配的catch会终止程序。相当于条件判断中的else。问题是不知道异常错误是什么
- 实际中抛出和捕获的类型不一定类型完全匹配,可以抛出派生类对象,使用基类来捕获,这个在实际生活中很实用。主要原因是:派生类可以赋值给基类
异常的匹配规则:
- 首先检查throw本身是否在try块内部,如果是,再在当前函数栈中查找匹配的catch语句。如果有匹配的直接跳到catch的地方执行
- 如果没有匹配的catch块,则退出但钱函数栈,在调用函数的栈中查找匹配的catch
- 如果到达main函数的栈,都没有匹配的catch,就会终止程序
- 上述沿着调用链查找匹配的catch块的过程叫栈展开。所以实际要最后要加一个catch(…)来捕获任意类型的异常,防止程序终止
- 找到匹配的catch会直接跳到catch语句执行,执行完后,会继续沿着catch语句后面执行
比如:
如果在中间匹配的:
结论:按照函数调用链,一层一层往外找,直到找到匹配的catch块,直接跳到匹配的catch块执行,执行完catch,会继续往catch块后面的语句执行。相当于没有找到匹配的函数栈帧被释放了
有可能单个的catch不能完全处理一个异常,在进行一些矫正处理后,需要交给更外层的调用链函数来处理。catch可以做完矫正操作,再将异常重新抛出,交给更上层的函数进行处理
由于抛异常只要找到匹配的catch就直接跳到catch块执行,没有找到对应catch的函数就不会继续执行。这样导致函数的执行流回很乱。可能会导致一些问题:
- 构造函数完成对象的构造和初始化,最好不要再构造函数中抛出异常,否则可能导致对象不完整或者没有完全初始化
- 析构函数主要完成资源的清理,最好不要在析构函数中抛异常,否则可能导致内存泄漏
- C++异常经常会导致资源泄漏问题。比如:在new和delete中抛出异常,导致new出来的资源没有释放,导致内存泄漏。在lock和unlock中抛出异常,导致锁没有释放,导致死锁
有两种解决办法:
- 将异常捕获,释放资源后,将锁重新抛出
- 使用RAII(资源获取就是初始化)的思想解决。定义一个类封装,管理资源。当要使用时实例化一个类对象,将资源传入,当退出函数,调用对象析构函数,释放资源
异常规格说明,是使函数调用者直到函数可能会抛出哪些异常。可以在函数后面接throw(异常类型),列出这个函数可能抛出的所有异常类型
在函数后面加throw()或者noexcept表示不抛异常
若没有接口声明表示,此函数可能会抛出任意类型的异常
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常 void fun() throw(A,B,C,D); // 这里表示这个函数只会抛出bad_alloc的异常 void* operator new (std::size_t size) throw (std::bad_alloc); // 这里表示这个函数不会抛出异常 void* operator new (std::size_t size, void* ptr) throw(); void* operator new (std::size_t size, void* ptr) noexcept;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 在实际中,并不是我们想抛什么异常就抛什么异常,这样会导致捕捉的时候不好捕捉。实际使用中,会建立一个继承体系,建立一个异常类,派生类继承这个类,来定义出不同的异常
- 到时候抛出异常,只需要用基类进行捕捉即可
基类可以相当于是一个框架,派生类是具体的异常。然后去具体实现异常的内容,然后抛异常只需要抛派生类,捕捉异常只需要捕捉基类即可
//基类 //异常 class Exception { public: Exception(const char* str = nullptr, int id = 0) :_errmsg(str) , _id(id) {} virtual void what()const = 0; protected: string _errmsg;//错误信息 int _id;//错误码 }; //派生类 //数据库异常 class SqlException :public Exception { public: SqlException(const char *str = nullptr, int id = 1) :Exception(str, id) {} virtual void what()const { cout << "error msg:" << _errmsg << endl; cout << "error id:" << _id << endl; } }; //网络异常 class HttpException :public Exception { public: HttpException(const char *str = nullptr, int id = 2) :Exception(str, id) {} virtual void what()const { cout << "error msg:" << _errmsg << endl; cout << "error id:" << _id << endl; } }; //缓存异常 class CacheException :public Exception { public: CacheException(const char *str = nullptr, int id = 3) :Exception(str, id) {} virtual void what()const { cout << "error msg:" << _errmsg << endl; cout<< "error id:" << _id << endl; } }; void test() { //当网络连接失败,抛出这个异常即可 //throw HttpException("Http fail", 2); //当缓存错误 //throw CacheException("Cache error", 3); //当数据库错误 throw SqlException("Sql error", 4); } int main() { try { test(); } //用基类捕捉即可 catch (const Exception& a) { a.what(); } catch (...) { cout << "unknow exception" << endl; } system("pause"); return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
在C++标准库中,异常是围绕下图组织的
unsigned short test()
{
unsigned short a = 0;
unsigned short b = 0;
cin >> a >> b;
if (a + b > 65535 )
{
//c++标准库中的异常类
throw overflow_error("overflow");
}
return a + b;
}
int main()
{
try
{
printf("%d\n", test());
}
//用基类捕捉
catch (const exception& a)
{
cout << a.what() << endl;
}
catch (...)
{
cout << "unknow exception" << endl;
}
system("pause");
return 0;
}
异常的优点:
- 异常对象定义完备之后,相比于错误码的方式,能让用户更加清楚的了解到自己遇到了什么类型的问题,更好定位程序的bug
- 函数错误码若遇到,需要层层向外返回;而异常则通过
catch
可以直接跳到对应处理位置- 第三方库包含异常,我们在使用类似于
boost/gtest
等第三方库的时候也需要使用对应的异常处理- 对于
T& operator[]
这种操作符重载,我们没办法很好地使用返回值来标识错误(因为不同类型的返回值不一样,没办法统一处理)这时候就可以用异常来抛出越界问题异常的缺点:
- 异常可能会导致程序到处乱跳(因为会跳到最近的
catch
位置)给观察错误情况增添了一些难度- 异常有一定性能开销(可忽略)
- 异常容易导致资源泄漏等等问题
- 异常依赖于用户编程规范,否则函数调用容易出现异常没有得到处理的问题
C++相比于其他语言异常机制问题:
- 其他语言与C++异常机制基本差不多
- C++标准库定义的不好,很多公司都是自己定义一套自己的异常库
- C++没有垃圾回收器,所以申请和释放内存要非常小心,容易泄露,需要RAII机制来处理