C++异常处理解析: 异常的引起(throw), 捕获(try catch)、异常安全

前言:

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

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中的原话是: 异常对象是经过复制被抛出表达式的结果建立, 该结果必须是能够复制的类型)ide

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

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

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必须最早出现, 否则永远都不可能执行到htm

3. catch(…) 这个语法表示catch捕获全部异常对象

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

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

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

C++ standard exception classes inheritance diagram

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

异常安全(内存泄露, 空指针等问题)

前言:

C++异常安全是针对C++异常处理带来的可能的隐患(内存泄露, 空指针等)而言的, 咱们知道异常一旦发生, 程序就会转移控制权, 若是在转移控制权的以前, 没有妥善处理, 好比忘记释放内存, 空指针等, 会形成严重的未定义行为或者资源泄露(内存泄露, 空指针等). 所谓异常安全, 就是为了保证即便是发生了异常, 这些相似的未定义(内存泄露, 空指针等)行为也不会发生.

C++异常安全概念:

咱们写程序的时候每每习惯按照假设程序正常运行的行为写代码, 管理资源等. 有时候也会写错误检测和处理的代码, 可是在这两个地方重叠时候, 也就是错误发生的时候的资源管理每每是容易被忽视的(下面立刻会给出两个例子, 内存泄露问题和空指针未定义行为问题).

异常安全是这么一个概念: 这个是指, 即便发生异常, 程序也能正确操做(异常发生之后要杜绝一切未定义的行为, 包括空指针, 内存泄露等, 即便异常发生, 那么相关实例仍是应该保持有效的状态).

C++异常安全要求:

C++异常安全通常有四个等级的要求(异常安全等级由低到高): 1. 没有任何异常安全保证, 也就是异常一旦发生, 可能形成程序行为的未定义; 2. 基本保证, 也就是异常发生的时候, 程序的行为仍是合法的, 状态也都是有效的, 行为是有定义的, 可是程序实例的状态有可能改变(仍旧合法) 3. 强保证(回滚保证), 这个等级就要求异常一旦发生而后进行处理了之后, 要么一次性所有成功, 要么就回滚到异常钱的原始状态(程序状态和异常发生之前如出一辙). 4. 保证不会有任何一方的发生.

这里面1是最不安全的, 不可取. 4基本上等级最强, 可是通常状况下不可能知足. 因此异常安全每每在2和3这两个等级间取舍. 等级3有可能会有额外的负担, 资源消耗等. 具体状况根据程序逻辑和实际状况判断取舍.

C++异常安全举例, 避免内存泄露:

C++异常安全其中一条重要的惯例, 是须要保证 若是发生异常, 被分配到的任何资源都适当地获得释放.  这个状况通常发生在动态分配内存的时候, 好比我程序里面有一段代码, 在第20行的时候首先动态分配了内存给一个指针p, 正常运行的话, 中间有一些处理代码, 而后到第40行delete [] p 释放内存, 程序正常运行的话没有问题, 可是要是在第20行到40行之间的代码出现了异常, 程序控制权转移给上级调用程序的时候, 这样的代码就有问题了, 此时, 做用域等效于已经到达了当前函数的结束, 全部局部变量或者实力都会调用自身的析构函数进行释放资源, 可是对动态分配内存的实例来说, 由于是直接异常跳转, 虽然做用域结束, 可是没有执行到delete进行手动释放, 这块动态内存将形成内存泄露.

那么比较好的保证这一类内存资源不泄露的异常安全的技术成为“资源分配即初始化”(参考RAII). 对于这句话“资源分配即初始化”我本身是这么理解的, 咱们要进行资源分配, 保证异常安全的作法不是普通的动态分配一块内存, 而是等效的初始化一个资源管理类的实例. 这就是所谓的“资源分配即初始化”, 也就是把资源分配等效的用初始化资源管理类来替代. 那么这里又提到了资源管理类, 咱们解释一下资源管理类以及“资源分配即初始化”到底好处在哪里. 基本上这点要求咱们设计一个资源管理类统一的管理资源的分配和释放, 更具体的, 利用构造函数分配资源, 利用析构行数释放资源. 这样作的好处呢, 是资源管理类自己是一个自动的局部对象, 无论是由于异常发生仍是正常的程序运行到了改局部对象的做用域的结束的时候, 这个类的析构函数都会被调用从而保证了资源的释放, 避免了内存泄露问题. C++里面提供了RAII的auto_ptr类, 就是一个资源管理类, 行为雷系指针. 咱们这里就不深刻研究它了.

C++异常安全举例, 避免空指针:

C++异常安全的另外一个常见的管理就是须要避免空指针. 这个状况的发生每每是咱们在动态分配内存的时候发生了异常. 好比咱们要分配p = new int[100], 这个时候要是内存不够, 那么就发生bad_alloc异常, p指针是空的NULL. 这个时候若是后面的代码依赖于p的未定义行为, 这样很容易致使程序的崩溃. 一个有效的避免空指针的作法就是, 在赋值以前就知道内存的分配是成功仍是失败, 一样能够利用咱们的资源管理类. 管理动态分配的内存, 若是分配成功, 那么将内存块的指针赋值给p, 若是失败, 那么抛出异常, 程序在p赋值前转移了控制权,此时p的值是不会改变的. 这样作就使得程序更加鲁棒(异常发生的时候, p的状态没有改变, 也没有产生未定义行为).

错误处理(返回值, 错误标志变量, 异常)

前言:

程序设计里面相当重要的一块就是错误处理, C++异常处理是一种面向对象的机制, 指望将错误处理和错误检测分离. 这里咱们结合其余两种错误处理方式(返回值, 错误标志变量)来分析一下不一样的错误处理(包括返回值判断,  错误标志变量, 异常处理机制)各有什么优缺点以及各自的适用环境.

函数返回值判断错误处理:

这种错误处理和判断的方法基本上是使用一组错误处理的常量, 而后经过函数返回值, 把错误信息返回给函数调用者. 好比以下简单的代码:

const int invalidPara = -2;

const int outOfRange = -3;
 const int other = -4;
int func(int para)
{
   if(invalid parameter)
       return invalidPara;
   do something here;
   if(out of range)
       return outOfRange;
   if(other error)
       return other;
}

这样的返回值判断的好处在于和系统API统一, 咱们知道WinAPI以及Linux下面的系统函数都是以返回0(零)表示程序正常运行, 返回非零值表示不一样的错误. 因此若是咱们也采用这样的返回值判断的话能够和系统调用统一块儿来.

可是返回值判断错误的限制以及缺点也是很明显的(我的不是很推崇用返回值, 可是也仍是要看具体状况). 首先呢, 返回值判断错误会破坏正常的返回值的做用, 使得函数调用不能被充分利用, 函数返回值不能做为其余表达式的组成部分, 由于这个返回值已经用来指示错误了而不是用来返回其余正常的计算结果, 即便能够既用于正常值计算又用于返回错误, 好比正常值都是正数, 错误值都是负数, 那这个结果仍是不能直接被用做任何计算, 首先仍是要判断这个是正常计算结果呢仍是一个错误信息, 这就形成了计算的不方便.

其次不少时候实际上是没办法使用返回值来判断错误信息的. 好比 1) 当func()返回类型是int的时候, 并且正常的结果的返回就是全部int型的值都有可能, 这个时候咱们其实无法找到一个很好的int value 做为indicatro来指示这是个错误返回还不是一个正常的结果. 2) 编写范型的时候好比return T, 那怎么利用返回值来判断? 这个时候由于咱们不明确T的类型, 因此也没有很好的办法利用一个明确的返回值来判断或者给出错误信息. 在这些状况下, 异常处理应该是更为合理的错误处理的方式. 咱们后面第三条会再讲到。接下来能够看看第二种错误处理机制.

错误标志变量判断:

这个类型的错误判断基本上能够用下面的这段程序表示. 也就是设置一个错误标志变量, 而后经过引用或者指针的形式传递给被调用的函数, 函数一旦发现错误就设置这个标志, 上层调用者经过检查这个标志变量来判断是否有错误发生.

int funcCallee(int para, int &errorFlag) {
    if(invalid parameter)
        set errorFlag and return;
    do something here;
    if(out of range)
        set errorFlag and return;
    if(other error)
        set errorFlag and return;
}

int funcCaller(int para) {
    int errorFlag = 0;
    int ret = funcCallee(1, errorFlag);
    check errorFlag;
}

这个方法的好处在于如今咱们的返回值值表示正常计算结果, 能够被方便的利用起来, 比起第一种利用返回值判断的话是一个比较明显的优点, 并且前面提到的两种不能使用返回值判断错误的状况(泛型, 正常结果返回涵盖全部整型), 咱们也可使用标志位. 由于表示为老是能够保证是int型的, 并且是不受函数的代码逻辑影响的, 基本上是一个独立的错误标志. 在我看来这种方法彷佛并无明显的缺陷. 我我的比较推重.

C++异常处理机制:

其实我以为C++的异常处理就是咱们这里说的第二种利用标志变量的面向对象版本的错误处理机制, 本质上彷佛没有太大区别. 固然异常处理还有复杂的多精细的多. 二者都是统一的独立于程序业务逻辑的错误处理机制. 好比无论程序干什么(泛型也好, 其余什么也好), 咱们遇到错误老是可以抛出一个异常, 终止当前函数, 把控制权转移给上层调用函数进行处理. 对应到咱们的第二种错误标志变量的话, 就是检测到异常或者错误的时候, 正确设置标识变量, 而后return, 控制权也转移给上层调用函数, 上层调用函数经过判断标志变量的值来进行处理. 从这个角度来说, 彷佛二者也没有太大区别.

另外一方面呢, 异常机制做为C++的一种语言级别的机制, 其实会有比较大的开销, 包括控制权的转移等等, 他的好处在于错误处理和错误逻辑分离的很清楚, 并且强制使用者必定要处理异常, 不然程序将最终终止. 可是方法二呢, 要是我忘记去检查那个错误标志变量了怎么办? 回答是不怎么办. 由于这个仅仅是代码级别的判断, 没有任何强制措施去要求必定要处理。 这个就是很危险的了. 因此异常机制(语言级别的判断)从这个角度来说也是比较好的一种错误处理的选择.

结束语:

这篇文章咱们仍是解析C++的异常处理机制, 这里咱们结合其余两种错误处理方式(返回值, 错误标志变量)分析了这些不一样的错误处理, 即返回值判断,  错误标志变量, 异常处理机制各有什么优缺点以及各自的适用环境.

相关文章
相关标签/搜索