C++Primer_Chap18_用于大型程序的工具_List01_异常处理_笔记

满足在大规模应用程序的在独立开发的子系统之间协同处理错误的能力的特殊要求。

  异常处理(exception handling)机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信兵做出相应的处理。异常使得我们能够将问题的检测和解决过程分离开来。程序的一部分负责检测问题的出现,然后解决该问题的任务传递给程序的另一部分。

抛出异常

  在C++中,我们通过抛出(throw)一条表达式来引发(raise)一个异常。被抛出的表达式的类型以及当前的调用链共同决定了那个处理代码(handler)将被用来处理该异常。被选中的处理代码是在调用链中与抛出对象类型最匹配的最近的处理代码。

  当执行一个throw时,跟在throw后面的语句将不再被执行。程序的控制权从throw转移到与之匹配的catch模块。这种转移有两个重要含义:

  • 沿着调用链的函数可能会提早退出
  • 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁

  因为跟在throw后面的语句将不再被执行,所以throw语句的用法有点类似return:它通常作为一个条件语句的一部分或者作为某个函数的最后(或者唯一)一条语句。

栈展开

  当抛出一个异常后,程序暂停当前函数的执行过程并立即开始寻找与异常匹配的catch子句。当throw出现在一个try语句块(try block)内时,检查与该try块关联的catch子句。如果没找到,进行栈展开(stack unwinding)过程。栈展开过程沿着嵌套好书的调用链不断查找,直到找到与异常匹配的catch子句,或许也可能一直没找到匹配的catch,则退出主函数后查找过程终止

  假设找到了一个匹配的catch子句,则程序进入该子句并执行其中代码。当执行完这个catch子句后,找到与try快关联的最后一个catch子句之后的点,并从这里继续执行

  如果没有找到匹配的catch子句,程序将退出。因为异常通常被认为是妨碍程序正常执行的事件,所以当找不到匹配的catch时,程序将调用标准库函数terminate

栈展开过程中对象被自动销毁

  在栈展开过程中,位于调用链上的语句块可能会提前退出,对象会自动销毁。如果异常发生在构造函数中,可能当前对象只构造了一部分,我们也需要确保已构造的成员能被正确销毁

析构函数与异常

  析构函数总会被执行,但函数中负责释放资源的代码可能被跳过。如果一个块分配了资源,并且在负责释放这些资源的代码前面发生了一次,则释放资源的代码将不会被执行。

  在栈展开的过程中,运行类类型的局部对象的析构函数。因为这些析构函数是自动执行的。所以它们不应该抛出异常。一旦在栈展开的过程中析构函数抛出了异常,并且析构函数自身没有捕获到该异常,则程序将被终止

异常对象

  异常对象(exception object)是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。因此,throw语句中的表达式必须拥有完全的类型。而且如果该表达式是类类型,则相应的类必须含有一个可访问的析构函数和一个可访问的拷贝或移动构造函数。如果该表达式时数组类型或者函数类型,则表达式将被转换成与之对应的指针类型。

  异常对象位于由编译器管理的空间中,编译器确保无论最终调用的是哪个catch子句都能访问该空间。当异常处理完毕后,异常对象被销毁。

  当一个异常被抛出时,沿着调用链的块将以此退出直至找到与异常匹配的处理代码。因此抛出一个指向局部对象的指针几乎肯定是一种错误的行为。

  当我们抛出一条表达式时,该表达式的静态编译类型决定了异常对象的类型。如果一条throw表达式解引用一个基类指针,而该指针实际指向的是派生类对象,则抛出的对象将被切掉一部分,只有基类部分被抛出

捕获异常

  catch子句(catch clause)中的异常声明(exception declaration)看起来像是只包含一个形参的函数形参列表。像在形参列表中一样,如果catch无须访问抛出的表达式的话,可以忽略捕获形参的名字。

  声明的类型决定了处理代码所能捕获的异常类型。这个类型必须是完全类型,可以是左值引用,但不能是右值引用。

  当进入一个catch语句后,通过异常对象初始化异常声明中的参数。异常声明的静态类型将决定catch语句所能执行的操作。如果catch的参数是基类类型,则catch无法使用派生类特有的任何成员。

  通常情况下,如果catch接受的异常与某个继承体系有关,则最好将该catch的参数定义为引用类型。

查找匹配的处理代码

  在搜索catch语句的过程中,我们最终找到的catch未必是异常的最佳匹配,挑选出来的应该是第一个与异常匹配的catch语句。异常和catch异常声明的匹配规则比实参和形参的匹配规则受更多的限制,需要类型的精确匹配:

  • 允许从非常量到常量的类型转换,即一个非常量对象的throw语句可以匹配一个接受常量引用的catch语句
  • 允许派生类向基类的类型转换
  • 数组被转换成指向数组(元素)类型的指针,函数被转换成指向该函数类型的指针
  • 除此之外,包括标准算术类型转换和类类型转换在内的其他所有转换规则都不能在匹配catch的过程中使用。

  如果在多个catch语句类型之间存在这继承关系,我们应该吧继承链最底端的类(most derived type)放在前面,将继承链最顶端的类(least derived type)放在后面

重新抛出

  一个catch语句通过重新抛出(rethrowing)的操作将异常传递给另外一个catch语句。这里的重新抛出仍然是一条throw语句,只不过不包含任何表达式:

throw;

  空的throw语句只能出现在catch语句或catch语句直接或间接调用的函数之类。如果在处理代码之外的区域遇到了空的throw语句,编译器将调用terminate。

  一个重新抛出语句并不指定新的表达式,而是将当前的异常对象沿着调用链向上传递:

catch( my_error &eObj) {
    eObj.status = errCodes::severeErr;
    throw;
}catch(other_error eObj) {
    eObj.status = errCodes::badErr;
    throw;
}

捕获所有异常的处理代码

  有些情况我们并不知道异常的类型,即使我们知道所有的异常类型,也很难为所有类型提供唯一一个catch语句。为了一次性捕获所有异常,我们使用省略号作为异常声明,这样的处理代码称为捕获所有异常(catch-all)

void manip() {
    try {
        //此处引发并抛出异常
    }
    catch (...) {
        //...
        throw;
    }
}

  如果有多个catch,catch(...)必须在最后的位置。出现在捕获所有异常语句后面的catch语句将永远不会被匹配。

函数try语句和构造函数

  由于在初始值列表抛出列表时构造函数体内的try语句块还未生效,所以构造函数体内的catch语句无法处理构造函数初始值列表抛出的异常。要想处理构造函数初始值抛出的异常,必须将构造函数写成函数try语句块(也称为函数测试块,function try block):

template <typename T>
Blob<T>::Blob(std::initializer_list<T> i1) try :
            data(std::make_shared<std::vector<T>>(i1) {

} catch( const std::bad_alloc &e)
    {
        handle_out_of_memory(e);    
    }

noexcept异常说明

  在C++11标准中,可以通过提供noexcept说明(noexcept specification)指定某个函数不会抛出异常。

void recoup(int) noexcept;
void alloc(int);
  • noexcept要么出现在函数的所有声明语句和定义语句中,要么一次也不出现
  • 应该在函数的尾置返回类型之前
  • 可以在函数指针的声明和定义中指定内noexcept
  • 在typedef或类型别名中不能出现noexcept
  • 成员函数的noexcept说明符需要跟在const及引用限定符之后,而在final、override后虚函数的=0之前

违反异常说明

  编译器不会在编译时检查noexcept说明。如果说明了noexcept的同时含有throw语句或调用了可能抛出异常的其他函数,编译器将顺利编译通过,并不会因为这种违反异常说明的情况而报错。一旦一个noexcept函数抛出了异常,程序就会调用terminate以确保遵守不在运行时抛出异常的承诺。一般noexcept可用在两种情况下:

  1. 确认函数不会抛出异常
  2. 不知道如何处理异常
void recoup(int) noexcept;
void recoup(int) throw();        //老版本等价声明

异常说明的实参

    noexcept说明符接受一个可选的实参,该实参必须能转换为bool类型;

void recoup(int) noexcept(true);
void recoup(int) noexcept(false);

noexcept运算符

  noexcept说明符的实参常常和noexcept运算符(noexcept operator)混合使用。noexcept运算符是一个一元运算符,返回bool类型的右值常量表达式,用于表示给定表达式是否会抛出异常,不会求其运算对象的值:

noexcept(recoup(i));    //不抛出异常为true,否则结果为false

noexcept(e);

    最后一行代码:当e调用的所有函数都做了不抛出说明且e本身不含throw语句,返回true

void f() noexcept(noexcept(g()));    //f与g的异常说明一样

异常说明与指针、虚函数和拷贝控制

  显式或隐式说明了指针可能抛出异常,则指针可以指向任何函数。但做了不抛出异常的声明的指针只能指向不抛出异常的函数.

  如果虚函数承诺了不抛出异常,则后续派生出来的虚函数也必须做出同样的承诺。如果基类的虚函数允许抛出异常,派生类可以允许和不允许抛出异常。

  当编译器合成拷贝控制成员时,同时也会生成一个异常说明。如果对所有成员和基类的所有操作都承诺了不抛出异常,则合成出来的成员是noexcept的。如果过合成成员调用的任意一个函数可能抛出异常,则合成的成员是noexcept(false)的。如果我们定义了一个析构函数但没有为其提供异常说明,则编译器合成一个。合成的异常说明将与假设由编译器为类合成析构函数时所得的异常说明一致。

异常类层次

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值