目录
一、C语言传统的处理错误的方式
二、C++异常概念
三、异常的使用
异常的抛出和捕获
如何就规范异常这个问题呢?
在函数调用链中异常栈展开匹配原则
异常的重新抛出
如何做到把资源释放了,还要让异常在最外面处理呢?
四、自定义异常体系
C++标准库的异常体系
五、异常安全
六、异常规范
七、异常的优缺点
C++异常的优点:
C++异常的缺点:
异常不仅仅是属于C++的一个概念,它是属于面向对象的概念。面向对象的语言处理错误的方式几乎都选择了异常。对比的就是C语言处理错误的方式。
try
{// 保护的标识代码
}catch( ExceptionName e1 )
{// catch 块
}catch( ExceptionName e2 )
{// catch 块
}catch( ExceptionName eN )
{// catch 块
}
eg:我们的代码就是主函数执行Func,Func中包含一个Division函数,如果除数等于0抛异常,不等于0则返回计算的结果。
double Division(int a, int b)
{// 当b == 0时抛出异常if (b == 0)throw "Division by zero condition!";elsereturn ((double)a / (double)b);
}
void Func()
{int len, time;cin >> len >> time;cout << Division(len, time) << endl;
}
int main()
{try {Func();}catch (const char* errmsg){cout << errmsg << endl;} return 0;
}
我们这里throw的就是一个字符串对象,我们是在main函数中处理的,try处理完之后就会自动跳到catch的地方,我们这里的catch就是直接捕获一个字符串,然后输出。
1.程序没有抛异常的执行逻辑
我们输入正确的数字,它是直接从23行跳转到29的,中间的catch并不会执行,因为我们现在的程序是正确的,也就是说如果你没有抛异常,catch的代码就跳过了。
2.程序抛异常后,代码的执行逻辑
程序抛了异常,直接从8行跳转到26行。也就是说如果代码抛了异常直接跳转到捕获他的地方。我们这里写的catch就是拿到这个字符串,并输出一下。
异常的抛出和匹配原则
eg:基于上面的代码进行修改
double Division(int a, int b)
{// 当b == 0时抛出异常if (b == 0)throw "Division by zero condition!";elsereturn ((double)a / (double)b);
}
void Func1()
{int len, time;cin >> len >> time;cout << Division(len, time) << endl;
}void Func2()
{int len, time;cin >> len >> time;if (time != 0){throw 1;}else{cout << len << " " << time << endl;}
}
int main()
{try {Func1();Func2();}catch (const char* errmsg){cout << errmsg << endl;} catch (int errid){cout << errid << endl; }catch (...) {cout << "unkown exception" << endl;}return 0;
}
我们在执行Func2的时候抛了异常,直接就从25行跳转到了45行,第一个catch就直接跳过,剩下的catch也不会执行,执行完当前的catch就直接跳转到51行结束程序。
这个调用链指的就是函数的调用。像我们上面的代码就有一个这样的调用链。main函数调用Func1,Func1调用Division,这就是一个调用链。在Division中可以进行抛异常。这个Division抛出的异常激活的可不一定是主函数里面的catch,我们上面的例子激活的都是主函数里面的catch是因为别的地方都没有对应的catch。
eg: Func1中也有捕获。
double Division(int a, int b)
{// 当b == 0时抛出异常if (b == 0)throw "Division by zero condition!";elsereturn ((double)a / (double)b);
}
void Func1()
{try{int len, time;cin >> len >> time;cout << Division(len, time) << endl;}catch (const char* errmsg){cout << errmsg << endl;}
}void Func2()
{int len, time;cin >> len >> time;if (time != 0){throw 1;}else{cout << len << " " << time << endl;}
}
int main()
{try {Func1();Func2();}catch (const char* errmsg){cout << errmsg << endl;} catch (int errid){cout << errid << endl; }catch (...) {cout << "unkown exception" << endl;}return 0;
}
抛出异常后是被Func1捕获到了,而不是被main函数所捕获。因为Func1和main函数中的catch都和这个异常匹配,但是Func1里所抛异常位置最近。
如果Func1中有捕获但是类型不匹配,那么就会往匹配的地方跳。
总结:
对于这条调用链,首先看抛出的异常和哪个函数中的catch匹配,和哪一个匹配就跳转到哪一个的catch中去;如果两个catch都匹配,那么谁的位置离所抛异常近,就跳转到哪个函数的catch中去
eg:
double Division(int a, int b)
{// 当b == 0时抛出异常if (b == 0){string str("除零错误");throw str;}else{return ((double)a / (double)b);}}
void Func1()
{int len, time;cin >> len >> time;cout << Division(len, time) << endl;
}int main()
{try{Func1();}catch (const string& errmsg){cout << errmsg << endl;}return 0;
}
main函数调用Func1,Func1调用Division。Division函数throw了一个str,但是这个str是一个临时对象,是一个局部栈帧的对象,出了作用域它就销毁了。而这个catch就类似于实参传给形参,这个catch捕获的并不是这个str,而是str的拷贝,这个拷贝对象的生命周期在被catch后被销毁。所以编译器不能直接把这个str扔给catch,throw看起来是直接从throw的地方跳转到catch,但实际包含的过程:执行流是直接跳转过去,编译器编译的时候,通过throw的类型依次进行检查,找到对应的地方,编译的时候就把这些东西检查好了;跳转的过程中Division的栈帧进行销毁,随着Func1的栈帧进行销毁,然后回到main,从catch开始执行。
有时候我们确实捕获了异常,但是我们不排除某些地方悄悄的给我们抛出异常。
eg:
double Division(int a, int b)
{// 当b == 0时抛出异常if (b == 0){string str("除零错误");throw str;}else{return ((double)a / (double)b);}
}
void Func1()
{try{int len, time;cin >> len >> time;cout << Division(len, time) << endl;}catch (int errid){cout << errid << endl;}
}void Func2()
{int len, time;cin >> len >> time;if (time != 0){throw 3.33;}else{cout << len << " " << time << endl;}
}
int main()
{try{Func1();Func2();}catch (const string& errmsg){cout << errmsg << endl;}catch (int errid){cout << errid << endl;}return 0;
}
Func2抛出1个3浮点数类型的异常。因为没有对应的catch,所以程序就直接崩溃了。
而且出现的这种警告,在我们使用windows软件的时候也会出现。出现这个问题的原因就是有异常没有被捕获。我们上面的代码根本问题就是catch的类型没有和throw的类型匹配。C++中规定,如果抛出的异常没有与之匹配的就会终止程序。但是我们更多情况下是不希望程序终止的。比如: 用微信聊天,你给某人发消息,发送失败,这就是微信调用发消息函数的这个接口调用失败,发送失败微信就会给你一个发送错误的小提示。但微信不会因为发生消息失败就直接跳出和我们例子类似的窗口,点一下就直接退出了。
为了防止这种情况的发生(某些地方抛出一些程序不认识的异常导致程序直接终止),所以就出现了...,这...可以匹配任意的类型,有匹配的异常就走匹配的,没匹配异常就走...,但是整个东西不好的地方在于我虽然捕获到异常没有让程序终止,但是我不知道这个异常是什么。
eg:基于上面代码增加一个catch...
eg:比如公司写一个服务器从上到下很多层
不管是哪一个层出来错误(抛异常)都不处理,最终异常就都到了网络层里面去了,一般会在网络这层统一捕获,捕获以后要记录日志。一般公司都会以类似这种的方式去设计。
再比如说公司比较大,每一个小组都是5个人,下面的小组你抛你的异常,我抛我的异常,一会抛字符串类型,一会抛自定义类型,我虽然可以把常见的类型都捕获了,但是对于自定义类型却很难办,所以随便抛异常会让最外层的人很难受,所以一般我们都会有异常规范的,不能想抛啥就抛啥
比如数据库持久化层抛出的时候就记录下SQL语句,缓存层记录下插入缓存的id是什么,业务逻辑层记录下业务逻辑干的是啥,网络层可能要记录下是什么网络请求......每一次都有对应的要求。
正常规范的代码几乎是不太可能去抛整形,字符串这样的异常的,一般是抛自定义类型。所以C++衍生出:抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获。实际中也都是这么干的。
比如:我catch一个Exception类的对象,throw的时候就可以跑Exception对象和Exception子类对象,也就是说你要抛各种类型的异常都可以,但是前提就是必须是Exception对象和Exception子类对象。
1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句 。如果有匹配的,则调到catch的地方进行处理。
2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
double Division(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Division by zero condition!";}return (double)a / (double)b;
}void Func()
{// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再// 重新抛出去。int* array = new int[10];int len, time;cin >> len >> time;cout << Division(len, time) << endl;cout << "delete []" << array << endl;delete[] array;
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;// 记录日志}return 0;
}
对于这份代码,如果Division抛了异常,那么在Func中,就不会执行206行以后的代码了,因为throw了异常就直接跳到main函数中catch的地方了,这个时候就会出现内存泄露,这个就是异常安全的问题。
针对这个问题应该如何解决呢?
1. 智能指针(这里我们先不介绍,在以后的文章会进行介绍)
2. 直接在Func里面捕获这个异常
这个时候因为在Func里直接就捕获了,所以catch以后的代码就继续正常执行。
但是这样处理存在一个不好的问题,因为我们之前说希望最外层统一进行处理异常,因为最外层有记录日志,都在最外层统一记录,这个Func中是没有记录日志的或者说在Func里记录日志非常的麻烦,我就是要去最外层记录日志。我早Func中拦截异常,不是要处理异常,而是要正常释放资源。
这种方法叫做异常的处理抛出。 捕获了释放了资源再次重新抛。
还有一种更简单的方式:
实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那么外层的调用者基本就没办法玩了,所以实际中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了。(异常一般是在比较大的项目中才会出现的)
基类一般包含这两个成员errmsg与id,比如说:微信发消息,如果是出现权限问题(本来我们是好友,但是对方把你拉黑了),我给你发消息就失败了,就直接报错,给一个提示“你已经不是对方的好友”。本质上,微信针对这个底层就是抛一个异常处理,微信上层就捕获一下,发现拿到的异常是正常的错误,然后就把这个错误显示出来。发消息发不出去还有一种可能是发生了网络错误,比如你手机没流量了或者你在电梯里面,导致你没信号或者信号不好,出现这种情况,并不是一下发不出消息就直接报错“网络错误”,而是第一次发不出去尝试第二次,第二次发不出去尝试第三次...比如产品经理在设计的时候要求网络不行的时候尝试10次,10次都发不出去才会显示网络错误。当发生其他错误就直接把错误展示出来;当发生网络错误,我还要调用这个接口重试几次,我不能因为出现这个异常了就把网络错误的信息直接展示出来。所以针对某种错误我们要进行特殊处理。我们怎么知道它是哪一种错误呢,这个时候,错误的编号就显得尤为重要,所以我不仅要有错误的描述,我还要有错误的编号。
所以errmsg就是一个字符串用来描述到底是什么错误,id也很重要,因为有时候要区分一些错误,针对某种错误进行特殊处理。
eg:模拟一个异常体系
// 服务器开发中通常使用的异常继承体系
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 SqlException : public Exception
{
public:SqlException(const string& errmsg, int id, const string& sql):Exception(errmsg, id), _sql(sql){}virtual string what() const{string str = "HttpServerException:";str += _errmsg;str += "->";str += _sql;return str;}
private:const string _sql;
};class CacheException : public Exception
{
public:CacheException(const string& errmsg, int id):Exception(errmsg, id){}virtual string what() const{string str = "CacheException:";str += _errmsg;return str;}
};
class HttpServerException : public Exception
{
public:HttpServerException(const string& errmsg, int id, const string& type):Exception(errmsg, id), _type(type){}virtual string what() const{string str = "HttpServerException:";str += _type;str += ":";str += _errmsg;return str;}private:const string _type;
};void SQLMgr()
{srand(time(0));if (rand() % 7 == 0){throw SqlException("权限不足", 100, "select * from name = '张三'");}throw "xxxxx"; //偷偷抛一个不知道的异常
}void CacheMgr()
{srand(time(0));if (rand() % 5 == 0){throw CacheException("权限不足", 100);}else if (rand() % 6 == 0){throw CacheException("数据不存在", 101);}SQLMgr();
}void HttpServer()
{srand(time(0));if (rand() % 3 == 0){throw HttpServerException("请求资源不存在",100,"get");}else if (rand() % 4 == 0){throw HttpServerException("权限不足", 101, "post");}CacheMgr();
}void ServerStart()
{while (1){this_thread::sleep_for(chrono::seconds(1));try{HttpServer();}catch (const Exception& e) // 这里捕获父类对象就可以{//子类重写了父类的虚函数,而且是父类的引用进行调用//符合多态的条件,所以抛的是谁的,调用的就是谁的cout << e.what() << endl;}catch (...){cout << "Unkown Exception" << endl;}}
}int main()
{ServerStart();return 0;
}
C++ 提供了一系列标准的异常,定义在库中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:
说明:实际中我们可以可以去继承exception类实现自己的异常类。但是实际中很多公司像上面一样自己定义一套异常继承体系。因为C++标准库设计的不够好用。
eg:标准库的异常体系
异常虽然用起来有些好处,但是异常用起来也会导致一些问题,这些问题就是异常安全的问题。
eg:比如一下这些代码。
没有异常我们只需要观察中间有没有return,只要没有return这种影响执行流的,代码一定会顺序执行,只要顺序执行,lock后一定会unlock,malloc,new以后一定会free,delete...但是有了异常就会影响执行流,所以就会出现死锁,内存泄露...这种异常安全的问题。
在比如中间调用了一个func(),要不要去捕获异常呢?如果要捕获,又要去补谁呢?所以就有了异常规范。
// 这里表示这个函数会抛出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();
如果大家都遵循这个异常规范,那么实际中,就可以明确知道该函数是否抛异常,也因此可以知道某处地方是没有问题的,异常安全也可以更好更轻松的去解决。但是现实终究是现实,就比如虽然有红绿灯,但是总有人会不遵守交通规则。加之因为C++兼容C语言的缘故,C++也不能强制实行异常规范这个事情。因此异常规范在实际中很难执行。
eg:库中的异常
C++11增加了一个关键字noexcept,只要你不抛异常就加一个noexcept,当然C++11同样兼容C++98。
// 1.下面这段伪代码我们可以看到ConnnectSql中出错了,先返回给ServerStart,ServerStart再
返回给main函数,main函数再针对问题处理具体的错误。
// 2.如果是异常体系,不管是ConnnectSql还是ServerStart及调用函数出错,都不用检查,因为抛
出的异常异常会直接跳到main函数中catch捕获的地方,main函数直接处理错误。
int ConnnectSql()
{// 用户名密码错误if (...)return 1;// 权限不足if (...)return 2;
}
int ServerStart() {if (int ret = ConnnectSql() < 0)return ret;int fd = socket() if(fd < 0)return errno;
}
int main()
{if(ServerStart()<0)...return 0;
}
//如果非要用返回错误码去处理,比如at(但是这些非常的不好)
int at(size_t pos, T& x)
{}
//成功返回0,失败返回-1,如果成功了,我拿的值在x里面,通过输出型参数去拿着值
总结:异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外OO的语言(面向对象语言)基本都是用异常处理错误,这也可以看出这是大势所趋。