新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > 异常机制简单探讨

异常机制简单探讨

作者:时间:2011-11-22来源:网络收藏

引 言
我们在编写软件时不但要追求代码的正确性,更要关注程序的容错能力,在环境不正确或操作不当时不能死机,更不能造成灾难性后果。程序运行时有些错误是不可避免的,如内存不足、文件打开失败、数组下标溢出等,这时要力争做到排除错误,继续运行。
传统做法是返回一个错误代码,调用者通过if等语句测试返回值来判断是否成功。这样做有几个缺点:首先,增加的条件语句可能会带来更多的错误;其次,条件语句是分支点,会增加测试难度;另外,构造函数没有返回值,返回错误代码是不可能的。
C++的为我们提供了更好的解决方法。处理的基本思想是:当出现错误时抛出一个,希望它的调用者能捕获并处理这个异常。如果调用者也不能处理这个异常,那么异常会传递给上级调用,直到被捕获处理为止。如果程序始终没有处理这个异常,最终它会被传到C++运行环境,运行环境捕获后通常只是地终止这个程序。异常使得正常代码和错误处理代码清晰地划分开来,程序变得非常干净并且容易维护。
但是如何合理地使用异常来达到预期的效果呢?MISRA C++给出了一些推荐的规则,帮助程序员更加合理、可靠地实现异常机制。下面将结合这些规则对异常机制进行

本文引用地址:http://www.eepw.com.cn/article/150036.htm

1 在恰当的场合使用恰当的特性
MISRA C++对异常的第1条规则就是:
规则15-0-1(不容讨论):异常机制只能用来处理错误。
异常处理的本质是控制流程的转移,但异常机制是针对错误处理的,仅在代码可能出现异常的情况下使用,不能用来实现普通的流程转移。
例如:


语法不会阻止你这样做,但杀鸡焉用牛刀。这样不但会降低程序的可读性,也会带来更大的开销。实际上,用一个的if语句就可以实现上述逻辑。同样,出于程序流程的清晰性考虑的还有:
规则15-0-3(强制):不允许通过goto或者switch语句跳转到try或catch语句块内。

2 正确地抛出异常
什么时候,什么地方,抛出什么样的异常,都是需要仔细考虑的。MISRA C++对此也作了相关规定。首先,来看一下抛出异常对象的类型中有哪些需要注意的地方。规则15-0-2(推荐):抛出的异常对象不应该是指针类型。
如果抛出的异常对象是个指针类型,指向的是动态创建的对象,那么这个对象应该由哪个函数来负责销毁,什么时候销毁,都很不清楚。比如说,如果是在堆中建立的对象,那通常必须删除,否则会造成资源泄漏;如果不是在堆中建立的对象,通常不能删除,否则程序的行为将不可预测。
规则15-1-2(强制):不能显式地把NULL作为异常对象抛出。
因为throw(NULL)=tbrow(0),因此NULL会被当作整型捕获,而不是空指针常量,这可能与程序员的预期不一致。
通常,很多函数都是基于function-try-block结构的,即函数体整个包含在一个函数try块中。而函数能抛出什么类型的异常对象,有以下规定:
规则15-5-2(强制):如果一个函数声明时指定了具体的异常类型,那么它只能抛出指定类型的异常。
规则15-4-1(强制):如果一个函数声明时指定了异常的类型,那么在其他编译单元里该函数的声明必须有同样的指定。
函数的代码结构如下:返回值类型函数名(形参表)throw(类型名表){函数体}
如果函数在声明时没有异常规范,那么它可以抛出任意类型的异常对象;如果异常类型为空,则表示不抛出任何类型异常。注意这两者之间的区别,前者指没有throw(类型名表)语句,而后者有throw(类型名表),只是类型名表为空。但如果声明时指定了异常的类型,那么它只能抛出指定类型的异常。
另外,函数原型中的异常声明要与实现中的异常声明一致,否则会引起异常冲突。由于异常机制是在运行出现异常时才发挥作用的,因此如果函数的实现中抛出了没有在其异常声明列表中列出的异常,编译器也许不能检查出来。当抛出一个未在其异常声明列表里的异常类型时,unexpected()函数会被调用,默认会导致std::bad_exception类型的异常被抛出。如果std::bad_exception不在异常声明列表里,又会导致terminate()被调用,从而导致程序结束。
对于什么时候能抛出异常,则有以下规定:
规则15-3-1(强制):异常只能在初始化之后而且程序结束之前抛出。
在执行main函数体之前,是初始化阶段,构造和初始化静态对象;在main函数返回后,是终止阶段,静态对象被销毁。在这两个阶段中如果抛出异常,会导致程序以不定的方式终止(这依赖于具体的编译器)。例如:

在这个例子中,catch块只能捕获上面try块中的异常。如果在对象c的构造函数或析构函数中抛出异常,并不能被main里的catch块捕获,而且会导致程序终止。
除了上述规则,还有以下两个规则需要注意:
规则15-1-1(强制):throw语句中的表达式本身不能引发新的异常。
如果在构造异常对象,或者计算赋值表达式时引发新的异常,那么新的异常会在本来要抛出的异常之前被抛出,这与程序员的预期不一致。
规则15-1-3(强制):空的throw语句只能出现在catch语句块中。
空的throw用来将捕获的异常再抛出,可以实现多个处理程序问异常的传递。然而,如果在catch语句外用,由于没有捕获到异常,也就没有东西可以再抛出,这样会导致程序以不定的方式终止(这依赖具体的编译器)。

3 合理地处理异常
由于后面的讨论多处涉及到“栈展开”这个概念,这里先解释一下。“栈展开”是异常机制中一个重要的过程:在逐层查找用来处理异常的catch子句时,因为异常而退出复合语句和函数定义,这个过程被称作“栈展开”。随着栈的展开,在退出的复合语句和函数定义中声明的局部变量的生命期也结束,而且这些局部类对象的析构函数也会被调用,这样能保证内存空间得到合理回收。栈展开的概念对于理解后面的内容很重要,我们通过一个具体例子进一步阐述。


当异常发生时,在函数调用链中逐层查找该异常的catch子句。在栈展开过程中函数foo()首先被检查到,因为产生异常的语句没有被放在try块中,所以不会在:foo()中查找针对该异常的catch子句。栈展开过程继续向上遍历函数调用链到达调用foo()的函数。然而在foo()带着这个未处理的异常退出之前,栈展开过程会销毁foo()中所有在异常产生之前被创建的局部类对象。结果就是:o1、o2的析构函数被调用,o3已经“死亡”,而o4还没“出生”。
顾名思义,“异常”就是程序运行出现了非预期的情况,或者说错误。因此,出现异常必须有针对地处理。对此,MISRA C++首先有如下规定:
规则15-3-4(强制):所有可能的流程中显式抛出来的异常都应该有一个类型兼容的处理程序。
规则15-3-2(推荐):至少要有一个处理程序来处理所有其他未针对处理的异常。
如果程序抛出一个没有被处理的异常,程序会终止,而终止前调用栈有没有被“展开”,动态对象能不能被析构,这些都依赖于编译器。上面两条规则规定了:不但预期抛出的异常要进行处理,其他可能被抛出的异常也要有相应的处理措施。请注意规则15-3-4中“类型兼容”的字眼,C++有非常灵活的类型兼容规则,尤其对于类。例如当异常对象是派生类时,“兼容类型”可以是派生类,也可以是基类。后面我们还会具体讨论这个问题。
一个try块后可以有多个catch块来捕获不同的异常。当出现异常时,catch处理程序按照其在try块后出现的顺序被逐个检查,只要找到一个匹配的异常类型,后面的异常处理都被忽略。因此,catch处理程序出现的顺序很重要。
规则15-3-6(强制):若一个try-catch语句块有多个处理程序,或者一个派生类和其部分或全部基类的function-try-block块有多个处理程序,处理程序的顺序应该是先派生类后基类。
规则15-3-7(强制):若一个try-catch语句块或者function-try-block块有多个处理程序时,catch(…)处理程序(捕获所有异常)应该放在最后。
这是因为根据类型兼容规则,异常对象为派生类时可以被针对基类的处理程序所捕获。如果针对基类的处理程序放在前面,后面针对派生类的处理程序就不会被执行到。同理,catch(…)处理程序能捕获所有类型的异常,在其后面所有的异常处理程序都不会被执行到。


上一页 1 2 下一页

关键词: 探讨 简单 机制 异常

评论


相关推荐

技术专区

关闭