C++的异常
1.C语言处理错误的方式
- 终止程序。例如:
assert
,断言为假
则终止进程 - 返回错误码。程序员自己去查找
错误码
对应的错误信息,使用广泛 C 标准库
中setjmp
和longjmp
组合
2. C++的异常处理
异常是一种处理错误
的方式,当一个函数发现自己无法处理
的错误时就可以抛出异常
,让函数的直接或间接
的调用者处理这个错误。
- throw:当问题出现时,程序会
抛出
一个异常。通过使用throw
关键字来完成。 - catch:在想要处理问题的地方,通过
异常处理程序
捕获异常,catch
关键字用于捕获异常,也可以有多个catch
进行捕获。 - try:
try
块中的代码
标识将被激活的特定异常
,它后面通常跟着一个或多个catch 块
。
try
块中放置可能
抛出异常的代码,try 块中的代码被称为保护代码
。使用 try/catch 语句的语法如下:
try
{
//保护的代码标识
}
catch (ExceptionName e1)
{
// catch 块
}catch (ExceptionName e2)
{
// catch 块
}catch (ExceptionName e3)
{
// catch 块
}
3.使用异常方法
3.1 抛异常和捕捉异常
- 异常是通过
抛出对象
而引发的,该对象的类型
决定了应该激活哪个catch
的处理代码。 - 被选中的处理代码是调用
链
中与该对象类型匹配
且离抛出异常位置最近
的那一个。 - 抛出异常对象后,会生成一个
异常对象的拷贝
,因为抛出的异常对象可能是一个临时对象
,所以会生成 一个拷贝对象
,这个拷贝的临时对象会在被catch
以后销毁,类似于函数的传值返回
。 catch
可以捕获任意类型
的异常。- 抛出和捕获的匹配原则有个
例外
,并不都是类型完全严格匹配
,可以抛出的派生类对象
,使用基类捕获。实际工程中,使用的很多。
在函数调用链中异常栈展开匹配原则
:
- 检查
throw
本身是否在try
块内部,如果存在则查找匹配的catch
语句。如果有匹配
的,则调到catch
的地方进行处理。 如果没有匹配的catch
则退出当前函数栈,继续在调用函数的栈
中进行查找匹配的catch
。 - 如果到达
main函数的栈
,依旧没有匹配的,则终止程序
。这个过程称为栈展开
。实际使用中我们最后都要加一个catch(...)
捕获任意类型
的异常,否则当有异常没捕获,程序就会直接终止。 - 找到匹配的
catch
子句并处理以后,会继续沿着catch
子句后面继续执行
。
double Div(double x1, double x2)
{
if (x2 == 0)
{
throw "Div zero mistakes";//抛出异常
}
else
{
return x1 / x2;
}
}
void Test()
{
try{
int x1;
int x2;
cin >> x1 >> x2;
Div(x1, x2);
}
catch (const char* msg){//捕获字符串类型的异常
cout << msg << endl;
}
catch (int msg){//捕获整型的异常
cout << msg << endl;
}
catch (...){//捕获任意类型的异常
cout << "unkown exception" << endl;
}
cout << "继续执行" << endl;//沿着catch子句后面继续执行
}
int main()
{
try {
Test();
}
catch (const char* msg){//匹配且离抛出异常位置最近的
cout << msg << endl;
}
return 0;
}
3.2 重新抛出异常
有可能单个的catch
不能完全处理
一个异常,在进行一些校正处理
以后,希望再交给外层
的调用链函数
来处理,catch
则可以通过重新抛出
将异常传递给更上层的函数进行处理。
double Div(double x1, double x2)
{
if (x2 == 0)
{
throw "Div zero mistakes";//抛出异常
}
else
{
return x1 / x2;
}
}
void Test()
{
int* arr = new int[10];
try{
int x1;
int x2;
cin >> x1 >> x2;
Div(x1, x2);
}
catch (...){/
cout << "delete arr[]" << endl;
delete[] arr;//释放数组之后重新抛出
throw;
}
cout << "继续执行" << endl;//沿着catch子句后面继续执行
}
void Func()
{
}
int main()
{
try {
Test();
}
catch (const char* msg){//匹配且离抛出异常位置最近的
cout << msg << endl;
}
return 0;
}
3.3 异常安全及规范
异常安全:
构造
函数完成对象的构造和初始化
,最好不要在构造函数中抛出异常
,否则可能导致对象不完整
或没有完全初始化
,可能会跳过某些代码。析构
函数主要完成资源的清理
,最好不要在析构函数内抛出异常
,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
C++
中异常经常会导致资源泄漏
的问题,比如在new
和delete
中抛出了异常,导致内存泄漏,在lock
和unlock
之间抛出了异常导致死锁
,C++
经常使用RAII
来解决。
异常规范:
- 异常规格说明的
目的
是为了让函数使用者知道该函数可能抛出的异常
有哪些。 可以在函数的后面接throw(类型)
,列出这个函数可能抛的所有异常类型
。 - 函数的后面接
throw()
,表示函数不抛异常
。 - 若无异常接口声明,则此函数可以抛
任何类型
的异常。
//表示这个函数会抛出int/string/char中的某种类型的异常
void Func1() throw(int, string, char);
//表示这个函数不会抛出异常
void Func2() throw();
//表示可以抛出任意类型的异常
void Func3();
4. 自定义异常
实际使用中,我们都会自定义自己的异常体系
进行规范
的异常管理,因为一个项目中如果大家随意抛异常
,那么外层
的调用者会存在很多catch类型
,所以实际中都会定义一套继承
的规范体系。这样大家抛出的都是继承的派生类对象
,捕获一个基类
即可。
class Exception {
protected:
string _errmsg;
int _id;
};
class SqlException : public Exception {};
class CacheException : public Exception {};
class HttpServerException : public Exception {};
int main() {
try{
// server.Start();
// 抛出对象都是派生类对象
}
catch (const Exception& e) //捕获父类对象就可以
{}
catch (...){//捕捉任意类型即可
cout << "Unkown Exception" << endl;
}
return 0;
}
5.C++标准库的异常体系
C++ 提供了一系列标准的异常,定义在<exception>
中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下图所示:
下表是对上面层次结构中出现的每个异常
的说明:
void Test()
{
try{
vector<int> v(10, 5);
//系统内存不够会抛异常
v.reserve(1000000000);
}
//父类子类同时存在,严格匹配
catch (const bad_alloc& e)//捕捉子类对象
{
cout << e.what << endl;
}
catch (const exception& e) //捕获父类对象
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
注:父类子类同时存在,严格匹配,跳转到子类的catch
。
6. C++异常的优缺点
优点:
- 异常
对象
定义好之后,相比错误码的方式可以清晰准确
的展示出错误的各种信息
,甚至可以包含堆栈调用
的信息,这样可以帮助更好的定位程序的bug
。 - 返回
错误码
的传统方式有个很大的问题就是在函数调用链中,深层的函数返回了错误,那么我们得层层返回
错误,这样最外层才能拿到错误。但如果是异常体系
,不管那个调用函数出错,都不用检查返回值,因为抛出的异常
会直接跳到main函数catch捕获
的地方,直接在main函数
处理错误即可。 - 很多的第三方库都包含
异常
,例如boost、gtest、gmock
等常用的库。 - 很多
测试框架
都使用异常,这样能更好的使用单元测试
等进行白盒的测试
。 - 部分函数使用异常可以更好处理错误。。例如
T& operator()
函数,如果pos
越界了只能使用异常或者终止程序
处理,因为T代表的自定义类型
没办法通过返回值
表示错误。
缺点:
- 异常会导致程序的
执行流乱跳
,非常的混乱,这会导致我们跟踪调试
时以及分析程序时比较困难。 - 异常会有一些
性能的开销
。 - C++没有
垃圾回收机制
,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁
等异常安全问题。 这个需要使用RAII
来处理资源
的管理问题。 - C++标准库的异常体系定义得不好,导致大家
各自定义各自的异常体系
,非常的混乱
。 - 异常尽量
规范使用
,如果随意抛异常,外层捕获的用户
会catch
很多类型。 - C++异常利大于弊。
异常规范有两点:
- 抛出异常类型都继承自一个
基类
。 - 函数
是否抛异常、抛什么异常,都使用 func() throw()
的方式规范化。