异常
传统C中处理错误的方式
以往我们在写C语言程序时,当用户的错误输入或者非期望结果发生时我们的程序有可能就会开始不正确的走向,此时我们为了程序的可靠性和健壮性往往需要对结果进行判断,并且防止一些非预期行为的发生。在C语言中我们程序在发生错误时我们往往会通过以下几种方式来阻止程序继续向错误方向执行,并且向外返回错误。
返回错误码
返回错误码就是我们常说的返回值,每个函数都有一个返回值,我们可以返回一个整形数据来告知外部执行的函数发生了什么问题。可是这样的返回错误码的方式不便于使用,我们为了搞懂错误是什么还不得不去查询手册,而且如果在主函数中返回的话程序会直接返回错误码终止程序。
终止程序
在C中有类似assert
的断言函数供我们判断状态,保证错误的发生,可是当不符合assert
的要求时,它会直接终止程序,并且给我们一个终止位置让我们自己去找错误,这不仅不便于查找错误甚至还让用户难以接收,谁都不想因为一点小错程序自己就会挂掉。
非本地跳转
在C语言的标准库中有这么一组函数setjmp
和longjmp
用于实现非本地跳转,setjmp
用于设置跳转点,longjmp
用于进行跳转(这里的跳转已经类似于Cpp的异常处理,异常处理也是在他们的基础上进行完善实现),但是他们两个并不常用。
Cpp异常处理
Cpp的异常处理更加人性化,更加方便,它可以帮助用户更加直观的理解错误,并且还能保证程序可以不会退出继续按照我们的要求继续执行
关键字
在Cpp中关于异常处理一共有三个关键字try,catch, throw
。
try
用于将有可能抛出异常的代码包裹,这些代码被称为保护代码。try
后面必须至少跟一个catch
。
throw
用于抛出异常,throw
必须在try
包裹的保护代码中使用才能起到正常的效果。
catch
用于捕获异常以及处理异常,跟在try
的后面。
使用格式1
2
3
4
5
6
7
8
9
10
11
12try
{
throw ...;
}
catch(...)
{
}
catch(...)
{
}
异常匹配规则
异常处理由使用throw
抛出一个对象而引发,抛出对象的类型决定了该执行哪个catch
语句中的内容。抛出的异常对象会以传值的方式传递给catch
语句,这里十分类似于函数的副本传参调用方式,因此传递给catch
的是抛出的异常对象的拷贝,之所以要拷贝是因为异常对象可能是个临时对象。传递给catch
的拷贝对象会在执行完catch
后释放。一般来说catch
的类型和抛出的对象类型并不需要完全相同的,比如我们可以用基类捕获派生类异常对象,这点十分实用。catch(...)
可以捕获任意类型异常对象,一般最后加上这个捕获来保证所有异常都能被捕获。
throw
语句后面的所有语句都不会再执行,会直接跳到try
后面最近的匹配的catch
语句,执行完catch
后会从最后一个catch
后开始执行。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#include <iostream>
#include <string>
using namespace std;
double Div(double num1, double num2)
{
if (num2 == 0)
{
throw string("Division by zero condition");
cout << "Here" << endl;//不会执行了
}
return num1 / num2;
}
int main()
{
try
{
Div(1, 0);//故意引发异常
}
catch(const string errmsg)
{
cout << errmsg << endl;
}
catch(...)
{
cout << "unknow error" << endl;
}
cout << "Here Start" << endl;//执行完异常处理后从这里开始继续执行
}
Division by zero condition
Here Start
栈展开
在函数中使用异常处理时,会按照以下流程去处理异常。
1、检查throw
是否在try
中。
2、在当前函数栈中寻找是否有匹配的catch
。
3、如果当前函数栈中有匹配的catch
则处理,处理完继续执行最后一个catch
后的语句。没有则跳出当前函数栈,前往上层调用函数栈继续寻找是否有匹配的catch
,如果某一层匹配,则处理异常,之后继续从当前栈中最后一个catch
后继续执行。
4、如果直到main
搜索完都没有匹配的catch
则终止程序。
以上这个过程称为栈展开。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#include <iostream>
#include <string>
using namespace std;
double Div(double num1, double num2)
{
if (num2 == 0)
{
throw string("Division by zero condition");
//cout << "Here" << endl;//不会执行了
}
//这里无法处理异常前往上层
return num1 / num2;
}
void Func()
{
try
{
Div(1, 0);
}//跳到这里处理异常
catch(const string errmsg)
{
cout << errmsg << endl;
}
catch(...)
{
cout << "unknow error" << endl;
}
cout << "Start Here" << endl;
}
int main()
{
Func();
}
Division by zero condition
Start Here
异常重新抛出
有时候一个异常一个catch
并无法成功处理,我们希望在本层catch
做简单处理后再抛出给上层进行处理,于是我们可以在catch
中重新抛出异常。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#include <iostream>
#include <string>
using namespace std;
double Div(double num1, double num2)
{
if (num2 == 0)
{
throw string("Division by zero condition");
//cout << "Here" << endl;//不会执行了
}
//这里无法处理异常前往上层
return num1 / num2;
}
void Func()
{
int* arr = new int[10];
try
{
Div(1, 0);
}//跳到这里处理异常
catch(const string errmsg)
{
//为了内存不泄露在这里先释放内存
cout << "delete[]" << endl;
delete[] arr;
//抛给上层处理
throw;
}
cout << "delete[]" << endl;
delete[] arr;
}
int main()
{
try
{
Func();
}
catch(const string errmsg)
{
cout << errmsg << endl;
}
}
delete[]
Division by zero condition
异常的安全与规范
在Cpp中抛出异常是不安全的,因为在throw
后的代码都不会再执行下去会打乱程序的执行顺序,因此强烈不建议在构造函数和析构函数中抛出异常,可能会导致对象无法完全初始化或者无法完全释放空间导致内存泄露的情况发生。
为了保证规范性,我们应该提前告诉用户这个函数会抛出哪些类型的异常,方便使用者调用这个函数及处理异常。为此我们可以在函数声明后面加上throw(异常类型...)
。1
2
3
4
5
6//这里表示这个函数会抛出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();
如果没有声明throw
则表示这个函数可以抛出任意类型的异常。
自定义异常体系
实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异 常,那么外层的调用者基本就没办法玩了,所以实际中都会定义一套继承的规范体系。这样大家抛出的都是 继承的派生类对象,捕获一个基类就可以了。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#include <iostream>
#include <string>
//服务器开发中通常使用的异常继承体系
class Exception
{
protected:
std::string _errmsg;
int _id;
//list<StackInfo> _traceStack;
// ...
};
class SqlException : public Exception
{
};
class CacheException : public Exception
{
};
class HttpServerException : public Exception
{
};
int main()
{
try
{
// server.Start();
// 抛出对象都是派生类对象
}
catch (const Exception &e) // 这里捕获父类对象就可以
{
}
catch (...)
{
std::cout << "Unkown Exception" << std::endl;
}
return 0;
}
}
标准库中的异常体系
标准库中有一套官方的一场体系供我们使用,其中一共有以下几个类,并且继承关系如下。
异常类的介绍。
我们也可以通过类来继承exception
类来自定义异常类。但是实际中我们很少使用库中的异常类,因为它实在不够好用。
异常的优缺点
优点
1、可以更好的知道错误信息,包括调用栈信息等,可以更好的定位bug。
2、可以更好的从调用栈深处返回错误信息方便在外层处理,而错误码就必须得层层返回错误码才可以进行处理。
3、很多第三发库都包括异常,通过异常我们可以更好的使用它们。
4、很多测试框架都在使用异常,方便进行软件测试。
5、部分函数使用异常更好处理,比如返回值不是整形的函数,或者是模板的函数,都无法通过错误码返回异常。
缺点
1、异常会导致程序执行流来回变动,不便于调试和跟踪程序。
2、会增加一些性能开销,虽然不大但还是提出来。
3、Cpp内存需要自己回收,因此抛出异常如果不注意处理会造成内存泄露,死锁等情况的发生,不过我们可以通过RAII
解决,学习成本高(RAII
在之后介绍)。
4、Cpp标准库中的异常体系并不好用,导致大家各用各的异常体系,十分混乱。
5、异常抛出必须要规范化使用,否则外层捕获将带来很大困难,可见Cpp的异常处理必须十分谨慎。