关于C++的“抛出异常”机制:
关键点:
(1)throw是将抛出的表达式的值拷贝到“异常对象”中,catch则是根据异常对象进行参数匹配并处理异常;
(2)throw可一次性跳出多层函数调用,直到最近一层的try语句,称为“栈展开”;
(3)catch捕获时是将异常对象与catch参数的进行 类型比较,而不是值比较,所以只要类型相同,就可以进入catch中处理。(例如throw抛出一个int类型的值,catch(int &i)就可以对其进行处理;或者throw抛出一个类对象,catch(Base& b)也可成功匹配)
所谓 “try”,就是 “尝试着执行一下”,如果有异常,则通过throw向外抛出,随后在外部通过catch捕获并处理异常。
0. 概述:
异常处理 将 问题的 “检测” 与 “解决” 过程分离开来。
程序的一部分负责检测问题的出现,然后解决该问题的任务传递给程序的另一部分。
检测环节无须知道处理模块的所有细节,处理模块也无须知道检测模块的细节。
这种机制允许在大型程序开发中 两个独立开发的部分能够在运行时 就出现的问题进行通信并做出相应的处理。
要想有效的使用异常处理,必须首先了解 当抛出异常时发生了什么、捕获异常时发生了什么,以及用来传递错误的对象的意义。
1. 抛出异常:
C++通过 “throw” 关键字 抛出一条表达式来触发一个异常。
throw的用法类似于return:
它通常作为 条件语句的一部分 或者 作为某个函数的最后一条语句,当throw执行时,跟在throw后面的语句将不再被执行,程序的控制权从throw转移到与之匹配的catch(catch可能是同一个函数中的局部catch,也可能位于调用链上的其他函数)。
try {
throw expression;
}
catch(case 1) {
}
catch(case 2) {
}
catch(case 3) {
}
...
1.1 throw的处理过程:(栈展开)
throw语句一般位于try语句块内,当throw抛出一个异常时,程序暂停当前函数的执行过程,并寻找与try语句块关联的catch语句(类似 switch…case…),
如果这一步没找到匹配的catch,且这一层的try语句外部又包含着另一层try,则在外层try中继续寻找匹配的catch,如果找不到,则退出当前函数,在当前函数的外层函数中继续寻找匹配的try与catch。
上述过程被称为“栈展开”(stack unwinding)过程。
栈展开 过程沿着嵌套函数的调用链不断查找,直到找到匹配的catch 子句为止;
或者一直没有找到匹配的catch,则退出主函数终止查找过程(调用标准库函数terminate)。
如果找到了一个匹配的catch子句,则程序进入该子句并执行其中的代码,执行完成后回到到这个 try…catch… 的最后一个catch之后的位置继续向下执行。
1.2 析构函数 与 异常:
当异常发生调用throw,后面的语句将不会被执行,退出作用域时,作用域的局部对象都将会被释放,对于类对象,退出作用域时将自动调用它的析构函数。
因此,如果析构函数中有抛出异常的流程,应该要在析构函数内部try捕获,并在析构函数内部得到处理。
1.3 异常对象:
在编译器的管理空间中,会维护一种“异常对象”,专门用于抛出异常时使用。
当发生异常时,编译器会用throw 抛出的表达式的值 对 “异常对象” 进行拷贝初始化,当异常处理完毕后,编译器会将“异常对象”销毁。
所以,基于 异常对象 的这种处理机制,对抛出异常的处理有几点限制:
① 如果throw抛出的表达式是类类型,则此类必须要有可访问的 拷贝构造函数和 析构函数;(因为对 异常对象 进行拷贝初始化 以及 释放 异常对象的时候需要调用)
② throw抛出的异常对象 不能是指向局部对象的指针;(因为throw退出作用域后,局部对象随之被释放掉,抛出指针到外层后将无法访问所指向的局部对象)
③ throw抛出的表达式 为 此表达式的 静态编译类型,如果抛出的是一个指向类对象的基类指针,则派生类部分将被截断,只有基类部分被抛出。
2. 捕获异常:
使用 catch子句 捕获异常。
2.1 catch的参数 与 函数参数 类似:
① 如果catch参数是非引用类型,则该参数是“异常对象”的一个副本,在catch语句内修改该参数实际上改变的是局部副本的值而非异常对象本身;
② 如果catch参数是引用类型,则该参数就是“异常对象”的一个别名。此引用同样适用于继承体系下的对象传递:基类类型引用可用于绑定派生类对象。
2.2 catch的匹配规则:
按照其出现顺序逐一匹配,但 允许类型转换、允许派生类向基类的类型转换、允许非常量向常量的类型转换,所以越是专门的catch越应该置于判断列表的前端。
(如果多个catch语句之间存在继承关系,应该将派生类放在前面,基类放在后面(继承链最低端的类放在前面,最顶端的类放在后面))
2.3 重新抛出:
在catch捕获异常并开始处理后,如果一个单独的catch不能完整的处理某个异常,则当前catch会 向调用链更上一层 重新抛出异常(rethrowing),这次是一个 空的 throw语句:
(空throw语句只能出现在catch语句中)
catch (my_error &eObj) {
throw ;
}
如果catch的参数是引用类型,则 catch可将异常的内容修改后再向上层抛出。
2.4 捕获所有异常:
catch(...)
表示捕获所有类型的异常:
try {
}
catch(...) {
}
3. 函数try语句块 与 构造函数:
如果想要处理 构造函数初始阶段(初始值列表中发生的错误)的异常,写法是:
Bob::Bob(string i1) try : data(i1) {
} catch(const bad_alloc &e) { handle_out_of_memory(e); }
注意 try 和 catch 的位置。
4. nonexcept异常说明:
明确指出某个函数不可能抛出异常,有助于简化调用该函数的代码。
如果我们能够提前知道这个函数没有throw机制,就不需要在调用该函数时编写任何异常处理的代码,否则如果有抛出异常而却没有进行捕获,则有可能造成程序调用 terminate 终止。
使用 nonexcept 表示函数不会抛出异常:
void recoup(int) nonexcept; //不会抛出异常
void alloc(int); //可能抛出异常
5. 异常类层次:
throw 可抛出 ① 自定义的异常对象,也可以抛出 ② 标准异常中的类型。
关于标准异常:
exception是所有异常的父类。
bad_cast : 通过dynamic_cast 抛出;
runtime_error : 运行时异常,包括 3个自子类:
overflow_error / underflow_error / range_error
logic_error : 逻辑错误,包括 4个子类:
domain_error / invalid_argument / out_of_range / length_error
bad_alloc : new失败时,会抛出bad_alloc;
exception 标准异常有个比较好用的功能是可以通过 e.what()
打印出出错的位置(获取一个标识异常的字符串),例如:
#include <iostream>
#include <deque>
int main() {
std::deque<int> mydeq;
mydeq.push_back(1);
mydeq.push_back(2);
try {
mydeq.at(2); //访问越界,deque内部实现了throw抛出异常
}
catch(const std::exception &e) {
std::cerr << "exception: " << e.what() << std::endl;
}
return 0;
}
输出结果:
exception: deque
std::cout
(标准输出) 与 std::cerr
(标准错误输出) 的区别:
二者都是默认向屏幕输出,不同点在于:
stdout是输出到磁盘文件,默认情况下stdout是行缓冲的,它的输出会先放在一个buffer里面,只有到换行的时候才会输出到屏幕上;
而stderr是无缓冲的,直接输出到屏幕上。
6. C++抛出异常 相比于 “return error”的好处:
https://bbs.csdn.net/topics/391839431
简言之:在多层函数调用时,throw使代码更简洁、异常处理更可靠:
① throw可以一下子跳出多层的函数调用,内层异常,本层次不是需要处处捕获;
② return error 只能返回一层,需要层层传递,或者使用全局变量记录错误码,如果一层忽略,就失去了处理的机会。
7. 实际开发中的抛出异常应用举例:
void CHttpQuery::_QueryCreateGroup(const string& strAppKey, Json::Value& post_json_obj, CHttpConn *pHttpConn) {
try {
}
catch(std::runtime_error msg) {
}
}
void CImConn::OnRead() {
try {
}
catch (CPduException& ex) {
log_error();
if(pPdu) {
delete pPdu;
pPdu = nullptr;
}
OnClose();
}
}
简单使用举例:
#include <iostream>
using namespace std;
int main() {
int ival = 5;
char cchar = 'c';
char dchar = 'd';
try {
throw cchar;
}
catch(int) {
cout << "int excpetion" << endl;
}
catch(char &c) {
if(c == 'c') {
cout << "cchar excpetion" << endl;
}
else if(c == 'd') {
cout << "dchar excpetion" << endl;
}
}
return 0;
}
输出结果:
cchar excpetion