对于可以预计的问题,相对于原始的if判断返回值方法,异常处理机制的主要目的,是「把问题的检测处理分离」,相互独立以后,可以降低耦合度,且相互不需要知道内部细节。

栈展开(stack unwinding)

每个函数/代码块(function/block)被调用时,会把局部变量压栈。
然而当异常被throw后,当前栈就直接释放,搜寻是否有匹配的catch。
如果当前的try块的catch不能匹配,则再退栈,返回上一层函数,搜索匹配的catch。直到有catch接住这个异常为止。
这个过程就是栈展开。
我们需要注意的是:

  1. 退栈的情况和函数调用完毕后的退栈情况一样,不要传出局部变量的引用或指针
  2. 抛出异常时,代码的行为类似于return,后面的代码都不执行。于是才用RAII管理资源是必须的。
  3. [不重要]退栈的过程中清理局部变量,会调用析构函数。所以,析构函数一般不抛异常。如果你要抛,自己处理掉。否则直接terminate。

异常对象

异常对象可以是标准库里面的exception类型,也可以是任意的类型。因为退栈,局部变量都会被销毁,这个对象要能让很多层以外的调用者知道是什么东西出了问题。
STL里面的类型大致分两种:runtime_error和logic_error
runtime主要是溢出、越界。指运行时错误。
logic主要是参数错误、范围错误。指代码的错误。

「注意」为了满足退栈后别人还能访问到这个对象的特性,异常对象被存放在编译器管理的内存空间中。在catch语句结束后销毁。

异常的捕获

可以说,catch是一种特殊的函数。根据被抛出的异常对象的类型,编译器裁决是否符合catch函数对参数的要求。catch语句与普通的函数调用的主要区别如下:
函数调用裁决时,允许最大限度的类型转化。而catch则非常严格,只允许子类到父类变量到常量数组函数名转指针这三种类型转化。
另外:

  1. 异常对象的形参,和函数形参相似,也是使用「拷贝初始化」的方式初始化的。所以,这个异常类如果是自己定义的,必须小心「拷贝构造」函数。
  2. 和函数调用一样,引用类型的参数会直接操作全局的异常对象。

额外的主题

rethrowing
对于不能完全处理的异常,可以处理一部分,然后再向上抛。语法就是只写一个throw;
捕获所有异常
catch函数类型要求严格,想要一个函数接住所有,这样:catch(...)