什么是异常
异常是面向对象的语言处理程序错误的一种方式。
与其形成对比的是C语言处理错误的方式:
1.assert,终止程序
这种方式会直接终止程序,比较激进。
2.返回错误码
只有错误码,要知道是什么意思还需要用户自己去找
3.C标准库中setjmp和longjmp组合
异常的处理方式是:当一个函数发现了自己无法处理的错误时,就可以抛出异常,让函数的直接或间接调用者处理这个错误。
异常的用法
通过三个关键字完成异常的检测,抛异常与捕获异常
throw:当问题出现时,可以throw抛异常,这个异常可以是任意类型的对象,那么就可以包含更多的错误信息。
catch:捕获并处理异常的地方,要和抛出的异常的类型匹配才可以捕获到,可以有多个catch一起捕获。
try:try块里面放着可能抛异常的代码,该区域里的代码被称为保护代码
异常的抛出和捕获的匹配原则
一.异常是通过抛出对象而引发的,该对象的类型决定了应激活那个catch的处理代码
#include <iostream>
#include <string>
using namespace std;
double DIV(int x, int y)
{
if (y == 0)
throw "除0错误";
return (double)x / (double)y;
}
void Func()
{
int x, y;
cin >> x >> y;
cout << DIV(x, y) << endl;
}
int main()
{
try {
Func();
}
catch (const string s) {
cout <<"string的catch:" << s.c_str() << endl;
}
catch (const char* errmsg)
{
cout <<"字符串的catch:" << errmsg << endl;
}
return 0;
}
上面的代码是通过调用函数Func输入x,y,然后调用DIV函数计算x/y的值,当DIV发现计算可能会有除0错误时,就会抛异常,抛出的异常时字符串类型的,外面的catch有string类型和字符串两个类型,运行一下看看现象
匹配的catch与抛异常的类型相匹配。
二.被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的一个
所谓调用链,以上面的代码为例,main函数调用Func函数,Func函数调用DIV函数,那么这三个函数之间的调用关系就像一个链子一样,称之为调用链( main()->Func->DIV )
函数调用链中异常栈展开匹配原则:
首先检查throw本身是否在try块内部,查找catch,如果没有,则退出当前函数栈,在调用函数的栈中寻找匹配 的catch,找不到就继续后退,如果到达main函数栈还没有找到匹配的catch,则终止程序(栈展开),如果找到匹配的catch并处理后,会继续沿着catch子句后面继续执行。
我们对上面的代码进行一点修改
double DIV(int x, int y)
{
if (y == 0)//发现除0错误,抛字符串类型的异常
throw "除0错误";
return (double)x / (double)y;
}
void Func()
{
int x, y;
cin >> x >> y;
try {
cout << DIV(x, y) << endl;//catch要在这个栈帧里捕获,那这个栈帧就要有try检测
}
catch (const char* errmsg)
{
cout << "Func的catch:" << errmsg << endl;
}
}
int main()
{
try {
Func();
}
catch (const char* errmsg)
{
cout <<"main的catch:" << errmsg << endl;
}
return 0;
}
Func函数和main函数都有能够匹配的catch,运行一下可以发现与其匹配的catch是距离抛出这个异常更近的Func里的catch。
三.抛出异常对象后,会生成一个异常对象的拷贝,因为大多数情况下抛出的异常是一个临时对象(抛异常后离开当前作用域被销毁),所以会生成一个拷贝对象,这个拷贝的零时对象的生命周期与捕获他的catch有关,被catch以后就会被销毁(与在那个栈帧无关)
四. catch(…)可以捕获任意类型的异常,但是我们无法得知捕获到的异常的类型
当有异常没有被捕获,就会导致程序中止
double DIV(int x, int y)
{
if (y == 0)//发现除0错误,抛字符串类型的异常
throw "除0错误";
if (y == 5)//模拟我可能不知道的抛异常情况
throw y;
return (double)x / (double)y;
}
void Func()
{
int x, y;
cin >> x >> y;
cout << DIV(x, y) << endl;//catch要在这个栈帧里捕获,那这个栈帧就要有try检测
}
int main()
{
try {
Func();
}
catch (const char* errmsg)
{
cout <<"main的catch:" << errmsg << endl;
}
return 0;
}
上面的程序我们模拟当y=5时,会抛一个整形的异常,而我们并不知道,所以没有捕获,当异常发生时,就会导致程序终止。
捕获是多加一个catch(…),就可以防止这种情况的发生。
int main()
{
try {
Func();
}
catch (const char* errmsg)
{
cout <<"main的catch:" << errmsg << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
*五.对于内置类型的异常,抛异常要求类型匹配,但是允许切片或切割的发生,也就是说可以抛派生类对象,用基类捕获。
异常的重新抛出
void Func()
{
int x, y;
cin >> x >> y;
int* array = new int[10];
cout << DIV(x, y) << endl;//catch要在这个栈帧里捕获,那这个栈帧就要有try检测
delete[] array;
}
看上面的代码,如果我们在抛异常之前开辟了一块内存,并且不巧在释放内存之前进行了抛异常,抛异常会直接跳到catch的位置,那就等于开辟的内存没有释放,就会导致内存泄漏的问题,这就叫异常安全问题。
这种情况的解决方法一般有三种:
1.可以采用与lock_guard与unique_lock原理类似的智能指针方法解决。
2.可以在抛异常的地方提前用catch截获异常,这样代码就会接着catch在当前栈帧下继续执行。
void Func()
{
int x, y;
cin >> x >> y;
int* array = new int[10];
try {
cout << DIV(x, y) << endl;
}
catch(const char* errmsg)
{
cout << "截获异常" << endl;
}
delete[] array;
}
这种方法虽然可以解决内存泄漏的问题,但是他在栈帧里面处理了异常,不能在外部统一处理异常,不利于项目的管理。
3.异常的重新抛出:可以先捕获异常,但是不处理,而是在catch块里面正常释放资源,然后再把异常重新抛出去。
void Func()
{
int x, y;
cin >> x >> y;
int* array = new int[10];
try {
cout << DIV(x, y) << endl;
}
catch(const char* errmsg)
{
cout << "只释放资源,不处理异常" << endl;
delete[] array;
throw errmsg;
}
}
可以用下列语句,把接收到的异常全部再次抛出
catch(...)
{
//释放资源
throw;
}
自定义类的异常继承体系
正常在项目中使用异常时我们希望能够在最外层统一的处理异常,这样也方便进行日志的记录,并且为了防止不同组的成员随意的抛异常,一般会统一规定大家都抛派生类的异常,然后统一同一个基类捕获
可以建立一个如上图一样的异常继承体系,假设我们有三个函数之间的调用存在嵌套关系,他们抛的异常都是基类的一个子类,在我们的处理函数上我们就可以使用基类去捕获他们的异常,并且其处理方法还可以写成虚函数,构成多态,这样每一个子类的处理方法都可以在子类里自己定义,统一调用基类处理时就可以根据不同的子类调用不同的方法,这种方案明显更优,下面是实现代码:
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
,_id(id)
{}
virtual string what() const
{
return _errmsg;
}
protected:
string _errmsg;//描述一个错误
int _id;//知道是哪一种错误,对其进行区分,以便让我们进行对应的处理
};
class Child1Exception : public Exception
{
public:
Child1Exception(const string& errmsg, int id, const string& s)
:Exception(errmsg, id)
, _s(s)
{}
virtual string what() const
{
string str("Child1Exception:");
str += _errmsg;
str += "->";
str += _s;
return str;
}
private:
const string _s;
};
class Child2Exception : public Exception
{
public:
Child2Exception(const string& errmsg, int id,const string& s)
:Exception(errmsg,id)
,_s(s)
{}
virtual string what() const
{
string str ("Child2Exception:");
str += _errmsg;
str += "->";
str += _s;
return str;
}
private:
const string _s;
};
class Child3Exception : public Exception
{
public:
Child3Exception(const string& errmsg, int id)
:Exception(errmsg,id)
{}
virtual string what() const
{
string str ("Child3Exception:");
str += _errmsg;
return str;
}
};
void Child3Server()
{
//...
srand(time(0));
if (rand() % 7 == 0)
{
throw Child3Exception("权限不足", 100);
}
}
void Child2Server()
{
//...
srand(time(0));
if (rand() % 5 == 0)
{
throw Child2Exception("被奇数整除", 100,"五");
}
else if (rand() % 6 == 0)
{
throw Child2Exception("被偶数整除", 101, "被六整除");
}
Child3Server();
}
void Child1Server()
{
//...
srand(time(0));
if (rand() % 3 == 0)
{
throw Child1Exception("被奇数整除",100,"三");
}
else if (rand() % 4 == 0)
{
throw Child1Exception("被偶数整除", 101, "被四整除");
}
Child2Server();
}
void ServerStart()
{
while (1)
{
this_thread::sleep_for(std::chrono::seconds(1));
try {
Child1Server();
}
catch (const Exception& e) // 这里捕获父类对象就可以
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
}
}
int main()
{
ServerStart();
return 0;
}
下面是代码的运行结果:
C++的标准库的抛异常就采用的就是类似于上面的异常处理体系,有一个exception的基类,STL里抛的各种异常都是它的子类。其结构如下
异常规范
1.为了让函数的使用者知道该函数可能抛出哪些类型的异常,可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型
void Func() throw(A,B,C,D);//可能抛出A,B,C,D类型的异常
2.如果函数后面没有加任何异常接口声明,则该函数可能抛出各种类型的异常
3.如果该函数不会抛任何异常,可以在函数后面加throw()
void Func() throw();
C++11支持了当函数不会抛异常可以在后面加noexcept
void Func() noexcept;
异常的优缺点
优点:
- 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
- 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误。
- 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
- 很多测试框架都使用异常,这样能更好的使用单元测试等进行白盒的测试。
- 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。
缺点:
- 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
- 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
- C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。
- C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
- 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛派生类。二、规范对函数异常接口的声明。
以上就是本篇的全部内容。