C++异常处理解析1: 异常的引发(throw), 捕获(try catch), 标准异常等

前言:

C++的异常处理机制是用于将运行时错误检测和错误处理功能分离的一种机制(符合高内聚低耦合的软件工程设计要求),  这里主要总结一下C++异常处理的基础知识, 包括基本的如何引发异常(使用throw)和捕获异常(try catch)相关使用注意点, 以及C++标准库提供的一套标准异常类和这些异常类的继承层级结构以及相关使用方法和常用习惯.

C++异常的引发(throw):

引发C++异常的语法就是使用throw语句: throw object; 注意这里throw抛出的是一个对象,也就是说是一个实例. 一旦抛出, 发生两件事情: 第一, C++异常机制开始寻找try catch模块, 寻找和抛出的对象的类型相匹配的catch子句找到处理代码进行异常的处理, 这个过程是一个栈展开的过程,也就是说C++讲先从当前的函数体里面寻找try catch模块, 如果没有, 则在调用当前函数(比如我们叫当前函数A)的函数(我们叫调用A的函数B)寻找处理代码(在B里面寻找), 一直寻找直到找到匹配的catch子句, 然后运行catch里面的代码, 运行完毕以后, 从这个匹配的catch后面的代码继续运行. 第二件事情是, 栈展开前面的所有函数作用域都失效(比如, A调用B, B调用C, C调用D, D调用E, E抛出异常同时在C找到了处理异常的catch子句, 那么D, E作用域失效, 等效于D, E运行到了函数结尾), 局部对象(自动释放内存的对象, 而不是那些动态分配内存的对象, 这一点和异常安全有关我们后面会提到)都将调用析构函数进行销毁.

注意点:

1. throw抛出的对象一定要是可以复制的(C++ Primer中的原话是: 异常对象是通过复制被抛出表达式的结果创建, 该结果必须是可以复制的类型)

  1. 不要抛出(throw)一个数组或者函数, 原因是, 和函数参数一样, 数组和函数类型实参, 该实参自动转换为一个指针.

3. C++异常说明: void func(int) throw(exception type list), 表明函数func会且仅会抛出list中列举的异常对象类型, throw()表示不会抛出任何异常(空异常类型列表).

C++异常规范Best Practice:

正对上述第3点, 我在思考一个问题, 如何从函数原型申明里面看出这个函数会不会抛出异常, 或者抛出哪些异常, 这个就牵扯到了异常规范了, 也就是上面的第三点, 我直觉就是最好每个函数都有异常规范跟着, 这样我就清楚了, 这个函数可能抛出什么异常, 结果网上一搜资料, 恰巧相反, 关于异常规范, 老鸟的建议是千万别用, 于是best practice应当是看到一个函数应该走好这个函数可能抛出任何异常的准备. 我也是才发现, 详见这个链接: “A Pragmatic Look at Exception Specifications“. 摘录一些关键的点以及各中原因:

The idea behind exception specifications is easy to understand: In a C++ program, unless otherwise specified, any function might conceivably emit any type of exception. Consider a function named Func() (because the name f() is so dreadfully overused):

// Example 1(a)
//
int Func();            // can throw anything

By default, in C++, Func() could indeed throw anything, just as the comment says. Now, often we know just what kinds of things a function might throw, and then it’s certainly reasonable to want to supply the compiler and the human programmer with some information limiting what exceptions could come tearing out of a function. For example:

// Example 1(b)
//
int Gunc() throw();    // will throw nothing

int Hunc() throw(A,B); // can only throw A or B

然后如果你自己读过这篇文章, 总结起来不建议用异常规范的理由有两点

  1. 首先, 异常规范并不保证, 那些没有在声明里面表明的异常不会被抛出, 编译器只是保证了这样的抛出一定会被检测到而已, 潜台词就是说你还是可以抛出, 只不过现在函数声明里面有说明你不能这么做, 那这就是运行时错误, 所以其实也更本就没有起到什么作用, 反而导致了程序在大多数情况下都terminate了.
  2. 同样是上面的原因, 这样编译器反而没有做任何优化, 反而更加费劲, 因为增加了unexpected()的负担.
  3. 假如你加一个throw()属性到你的永远不会抛出异常的函数中,编译器会非常聪明的知道代码的意图和决定优化方式, 这一点可能才是编译器优化的方面, 也是那篇文章唯一建议可以用的异常规范, 甚至, 那个作者建议连这个都不要用, 但是如果用, 这个是唯一可以用的, 只有这个可能还有点编译器优化的作用.

C++异常的捕获(try catch):

如果要试图捕获C++异常, 那么将可能抛出(throw)异常的代码块放到try{}里面, 在try{} 后面跟上catch(exception e) {}, 这里的e是一般的异常对象, C++异常处理通过抛出对象的类型来判断决定激活哪个catch处理代码. 具体语法可以参见任何一本C++的书籍. 这里主要提几点注意点:

  1. 讲throw的时候也提到了, catch是一层一层catch(栈展开), 当寻找到main里面也没有catch捕获的时候, C++机制一般将调用terminate终止进程(abort)

2.  catch子句列表中, 最特殊的catch必须最先出现, 不然永远都不可能执行到

  1. catch(…) 这个语法表示catch捕获所有异常

  2. 在catch里面使用throw ;这条语句将重新抛出异常对象, 改异常对象是和捕获的一场对象同一个对象(catch中可以修改这个对象)

C++标准异常介绍(继承层次结构等):

C++标准库提供了以下的标准异常类, 他们的继承层次结构如下(参考: Chapter 17: Advanced C++ Topics III). 比较好的写异常的做法是继承这些C++标准的异常类, 然后定义一组适合自己应用的异常处理对象集合. 

C++ standard exception classes inheritance diagram

结束语:

C++的异常处理机制主要用于将错误检测和错误处理功能分离, 从而达到低耦合的要求, 这篇文章主要总结了一下C++异常处理的基础知识, 从如何使用throw引发异常, 使用try catch等捕获异常到C++标准库提供的一套标准异常类和这些异常类的继承层级结构, 主要给出了相关使用方法和注意点以及一些程序设计的良好习惯. 文章全凭本人自己的理解原创行文, 如有不当之处, 在所难免, 还请不吝指正.

(全文完,转载时请注明作者和出处)


(转载本站文章请注明作者和出处 烟客旅人 sigmainfy — http://www.sigmainfy.com,请勿用于任何商业用途)

Written on September 26, 2013