异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理。
异常使得能够将问题的检测与解决过程分离开来:程序的一部分负责检测问题的出现,然后解决该问题的任务传递给程序的另一部分。检测环节无须知道问题处理模块的所有细节,反之亦然。
抛出异常
C++ 中,通过抛出(throwing)一条表达式来引发(raised)一个异常。
被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码(handler)将被用来处理该异常。
被选中的处理代码是在调用链中与抛出对象类型匹配的最近的处理代码。
其中,根据抛出对象的类型和内容,程序的异常抛出部分将会告知异常处理部分到底发生了什么错误。
当执行一个 throw
时,跟在 throw
后面的语句将不再被执行。相反,程序的控制权从throw
转移到与之匹配的 catch
模块。
该 catch
可能是同一个函数中的局部 catch
,也可能位于直接或间接调用了发生异常的函数的另一个函数中。控制权从一处转移到另一处,这有两个重要的含义:
1. 沿着调用链的函数可能会提早退出。
2. 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。
因为跟在 throw
后面的语句将不再被执行,所以 throw
语句的用法有点类似于 return 语句:它通常作为条件语句的一部分或者作为某个函数的最后(或者唯一)一条语句。
栈展开
当抛出一个异常后,程序暂停当前函数的执行过程并立即开始寻找与异常匹配的 catch
子句。当throw
出现在一个 try
语句块(try block)
内时,检查与该try
块关联的catch
子句。
如果找到了匹配的catch
,就使用该catch
处理异常。
如果这一步没找到匹配的catch
且该try
语句嵌套在其他try
块中,则继续检查与外层try
匹配的catch
子句。
如果还是找不到匹配的 catch
,则退出当前的函数,在调用当前函数的外层函数中继续寻找。
如果对抛出异常的函数的调用语句位于一个try
语句块内,则检查与该try
块关联的catch
子句。如果找到了匹配的catch
,就使用该catch
处理异常。否则,如果该try
语句嵌套在其他try
块中,则继续检查与外层try
匹配的catch
子句。如果仍然没有找到匹配的catch
,则退出当前这个主调函数,继续在调用了刚刚退出的这个函数的其他函数中寻找,以此类推。
上述过程被称为栈展开 过程。沿着嵌套函数的调用链不断查找,直到找到与异常匹配的catch子句为止;或者一直没找到匹配的 catch 退出主函数后查找过程中值。
假设找到了一个匹配的 catch
子句,则程序进入该子句并执行其中的代码。当执行完这个catch
子句后,找到与try
块关联的最后一个 catch
子句之后的点,并从这里继续执行。
如果没找到匹配的 catch
子句,程序将退出。
因为异常通常被认为是妨碍程序正常执行的事件,所以一旦引发了某个异常,就不能对它置之不理。当找不到匹配的 catch
时,程序将调用标准库函数 terminate
,顾名思义,terminate
负责终止程序的执行过程。
一个异常如果没有被捕获,则它将终止当前的程序。
栈展开过程中对象被自动销毁
如果栈展开过程中退出了某个块,那么在这个块中创建的局部对象将会被销毁。
析构函数与异常
析构函数总是会被执行的,但是函数中负责释放资源的代码却可能被跳过。
栈展开的过程中,类类型的局部对象的析构函数会被执行以销毁该对象。析构函数不应该抛出异常。如果析构函数需要执行某个可能抛出异常的操作,该操作应该放在 try
语句块中并在析构函数内部得到处理。
原因:当析构函数抛出了异常,又没有在析构函数内部完成处理的话,析构函数将会提早退出,导致没有完成对象的销毁工作。
异常对象
异常对象是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。因此,throw
语句中的表达式必须拥有完全类型。
而且如果该表达式是类类型的话,则相应的类必须含有一个可访问的析构函数和一个可访问的考贝或移动构造函数。如果该表达式是数组类型或函数类型,则表达式将被转换成与之对应的指针类型。
异常对象位于编译器管理的空间中,编译器确保无论调用的是哪个 catch
子句,都能访问该空间。当异常处理结束后,该异常对象被销毁。
抛出指针要求在任何对应的处理代码存在的地方,指针所指的对象都必须存在。
捕获异常
catch
子句中的异常声明看起来像是只包含一个形参的函数形参列表。像在形参列表中一样,如果catch
无须访问抛出的表达式的话,则可以忽略捕获形参的名字。
声明的类型决定了处理代码所能捕获的异常类型。这个类型必须是完全类型,它可以是左值引用,但不能是右值引用。
当进入一个catch
语句后,通过异常对象初始化异常声明中的参数:
如果catch
的参数类型是非引用类型,则该参数是异常对象的一个副本,在catch
语句内改变该参数实际上改变的是局部副本而非异常对象本身;
相反,如果参数是引用类型,则和其他引用参数一样,该参数是异常对象的一个别名,此时改变参数也就是改变异常对象。
如果 catch
的参数是基类类型,则可以使用派生类类型的异常对象对其进行初始化。此时,如果 catch
的参数是非引用类型,则异常对象会被切掉一部分。如果 catch
的参数是基类的引用,该参数将以常规方式绑定到异常对象上。
异常声明的静态类型将决定 catch
语句所能执行的操作。
如果 catch
接受的异常与某个继承体系有关,最好将该 catch
的参数定义为引用类型。
查找匹配的处理代码
在搜寻catch
语句的过程中,最终找到的catch
未必是异常的最佳匹配。相反,挑选出来的应该是第一个与异常匹配的catch
语句,越是专门的catch
越应该置于整个catch
列表的前端。
当程序使用具有继承关系的多个异常时必须对catch
语句的顺序进行组织和管理,使得派生类异常的处理代码出现在基类异常的处理代码之前。
原因: catch
语句是按照其出现的顺序逐一进行匹配的
与实参和形参的匹配规则相比,异常和 catch
异常声明的匹配规则更严格一些,可以进行以下三种转换:
1. 允许从非常量向常量的类型转换;
2. 允许从派生类向基类的类型转换;
3. 数组被转换为指向数组(元素)类型的指针,函数被转换成指向该函数类型的指针。
重新抛出
有时,一个单独的catch
语句不能完整地处理某个异常。在执行了某些校正操作之后,当前的catch
可能会决定由调用链更上一层的函数接着处理异常。
一条catch
语句通过重新抛出(rethrowing)的操作将异常传递给另外一个catch
语句。这里的重新抛出仍然是一条throw
语句,只不过不包含任何表达式: throw;
空的throw
语句只能出现在 catch
语句或catch
语句直接或间接调用的函数之内。如果在处理代码之外的区域遇到了空throw
语句,编译器将调用terminate
。
捕获所有异常的处理代码
使用省略号作为异常声明,可以一次性捕获所有异常。形如catch(...)
。
cpp
//catch(...)通常与重新抛出语句一起使用,其中 catch 执行当前局部能完成的工作,随后重新抛出异常
void manip(){
try {
//这里的操作将引发并抛出一个异常
}
catch(...){
//处理异常的某些特殊操作
throw ;
}
}
如果catch(...)
与其他几个 catch
语句一起出现,则 catch(...)
必须在最后的位置。出现在捕获所有异常语句后面的catch
语句将永远不会被匹配。
函数 try 语句块与构造函数
通常情况下,程序执行的任何时刻都可能发生异常,特别是异常可能发生在处理构造函数初始值的过程中。
构造函数在进入其函数体之前首先执行初始值列表。因为在初始值列表抛出异常时构造函数体内的try
语句块还未生效,所以构造函数体内的catch
语句无法处理构造函数初始值列表抛出的异常。
处理构造函数初始值异常的唯一方法就是将构造函数写成函数 try 语句块。
cpp
template <typenameT>
Blob<T>::Blob(std::initializer_list<T> il)
try:
data (std: :make shared<std: :vector<T>>(il)){
/*空函数体*/
}catch (const std::bad_alloc &e){ handle_out_of_memory(e);}
注意:关键字try
出现在表示构造函数初始值列表的冒号以及表示构造函数体的花括号之前。与这个try
关联的catch
既能处理构造函数体抛出的异常,也能处理成员初始化列表抛出的异常。
noexcept 异常说明
noexcept
异常说明可以指定某个函数不会抛出异常。其形式是 关键字 noexcept 紧跟在函数的参数列表后面:
cpp
void recoup(int) noexcept;//不会抛出异常
编译器并不会在编译时检查noexcept
说明。实际上,如果一个函数在说明了noexcept
的同时又含有throw
语句或者调用了可能抛出异常的其他函数,编译器将顺利编译通过。并不会因为这种违反异常说明的情况而报错。
异常说明的实参
noexcept
说明符接受一个可选的实参,该实参必须能转换为bool
类型:如果实参是true
,则函数不会抛出异常;如果实参是false
,则函数可能抛出异常:
cpp
void recoup(int) noexcept (true); //recoup不会抛出异常
void alloc(int) noexcept (false); //alloc可能抛出异常
noexcept运算符
noexcept
说明符的实参常和 noexcept
运算符混合使用。
noexcept
运算符是一个一元运算符,它的返回值是一个bool
类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。noexcept
不会求其运算对象的值。
cpp
noexcept (recoup(i))//如果recoup不抛出异常则结果为true;否则结果为false
noexcept
有两层含义:1.当跟在函数参数列表后面时它是异常说明符;2.当作为noexcept
异常说明的bool实参出现时,它是一个运算符。
异常说明与指针、虚函数和拷贝控制
如果函数做了不抛出声明,那么指向它的函数指针也必须做不抛出声明。
如果一个虚函数做了不抛出声明,派生的函数也必须做不抛出声明。
异常类层次
类型exception
仅仅定义了拷贝构造函数、拷贝赋值运算符、一个虚析构函数和一个名为what
的虚成员。其中 what
函数返回一个const char*
,该指针指向一个以unll
结尾的字符数组,并且确保不会抛出任何异常。
类 exception
、 bad_cast
和 bad_alloc
定义了默认构造函数。
类 runtime_error
和 logic_error
都没有默认构造函数,但有一个接受 C 风格字符串或标准库 string 类型实参的构造函数。
实际的应用程序常常会自定义 exception 的派生类来扩展继承体系。