1. 首页
  2. 业界

C语言代码中异常的处理机制

  编写软件过程中不但要追求代码的正确性,更要关注程序的容错能力,在环境不正确或操作不当时不能死机,更不能造成灾难性后果。程序运行时有些错误是不可避免的,如内存不足、文件打开失败、数组下标溢出等,这时要力争做到排除错误并记录错误,但是同时要保证项目的正常运行。


  传统做法是返回一个错误代码,调用者通过if等语句测试返回值来判断是否成功。这样做有几个缺点:首先,增加的条件语句可能会带来更多的错误;其次,条件语句是分支点,会增加测试难度;另外,构造函数没有返回值,返回错误代码是不可能的。


  C++的异常机制为我们提供了更好的解决方法。异常处理的基本思想是:当出现错误时抛出一个异常,希望它的调用者能捕获并处理这个异常。如果调用者也不能处理这个异常,那么异常会传递给上级调用,直到被捕获处理为止。如果程序始终没有处理这个异常,最终它会被传到C++运行环境,运行环境捕获后通常只是简单地终止这个程序。异常机制使得正常代码和错误处理代码清晰地划分开来,程序变得非常干净并且容易维护。


  但是如何合理地使用异常机制来达到预期的效果呢?博洋教育多年的c++培训和开发经验给出了一些推荐,帮助程序员更加合理、可靠地实现异常机制。下面将结合这些规则对异常机制进行简单的探讨。


  1 在恰当的场合使用恰当的特性


  这一条是不容讨论的,异常处理的本质是控制流程的转移,但异常机制只能用来处理错误,仅在代码可能出现异常的情况下使用,不能用来实现普通的流程转移。这样虽然会降低程序的可读性,也会带来更大的开销,但是出于程序流程的清晰性考虑是必须的:


  2 正确地抛出异常


  什么时候,什么地方,抛出什么样的异常,都是需要仔细考虑的。首先,来看一下抛出异常对象的类型中有哪些需要注意的地方。切记抛出的异常对象不应该是指针类型。


  如果抛出的异常对象是个指针类型,指向的是动态创建的对象,那么这个对象应该由哪个函数来负责销毁,什么时候销毁,都很不清楚。比如说,如果是在堆中建立的对象,那通常必须删除,否则会造成资源泄漏;如果不是在堆中建立的对象,通常不能删除,否则程序的行为将不可预测。还有不能显式地把NULL作为异常对象抛出。因为throw(NULL)=tbrow(0),因此NULL会被当作整型捕获,而不是空指针常量,这可能与程序员的预期不一致。


  另外,函数原型中的异常声明要与实现中的异常声明一致,否则会引起异常冲突。由于异常机制是在运行出现异常时才发挥作用的,因此如果函数的实现中抛出了没有在其异常声明列表中列出的异常,编译器也许不能检查出来。当抛出一个未在其异常声明列表里的异常类型时,unexpected()函数会被调用,默认会导致std::bad_exception类型的异常被抛出。如果std::bad_exception不在异常声明列表里,又会导致terminate()被调用,从而导致程序结束。


  对于什么时候能抛出异常,则有以下规定:异常只能在初始化之后而且程序结束之前抛出。


  3 合理地处理异常


  由于后面的讨论多处涉及到”栈展开”这个概念,随着栈的展开,在退出的复合语句和函数定义中声明的局部变量的生命期也结束,而且这些局部类对象的析构函数也会被调用,这样能保证内存空间得到合理回收。栈展开的概念对于理解后面的内容很重要,我们通过一个具体例子进一步阐述。


  当异常发生时,在函数调用链中逐层查找该异常的catch子句。在栈展开过程中函数foo()首先被检查到,因为产生异常的语句没有被放在try块中,所以不会在:foo()中查找针对该异常的catch子句。栈展开过程继续向上遍历函数调用链到达调用foo()的函数。然而在foo()带着这个未处理的异常退出之前,栈展开过程会销毁foo()中所有在异常产生之前被创建的局部类对象。结果就是:o1、o2的析构函数被调用,o3已经”死亡”,而o4还没”出生”。


  如果程序抛出一个没有被处理的异常,程序会终止,而终止前调用栈有没有被”展开”,动态对象能不能被析构,这些都依赖于编译器。上面两条规则规定了:不但预期抛出的异常要进行处理,其他可能被抛出的异常也要有相应的处理措施。请注意规则15-3-4中”类型兼容”的字眼,C++有非常灵活的类型兼容规则,尤其对于类。例如当异常对象是派生类时,”兼容类型”可以是派生类,也可以是基类。后面我们还会具体讨论这个问题。


  当异常抛出时,会进行栈展开。如果在某个析构过程中引发没有被处理的异常,程序将会以不定的方式终止。析构函数抛出异常的问题在很多C++的书中都有讨论,概括来说:析构函数应尽可能地避免抛出异常,如果的确无法避免,则析构函数自己应该包含处理所有可能抛出的异常的代码。


  4 小结


  异常机制是C++崭新而高级的特性之一。与其他C++特性一样,C++标准并没有规定应该如何来实现异常机制,这依赖于具体的编译器。异常机制是有代价的,它会增加代码大小和运行开销。


  可以预见,随着嵌入式产业的飞速发展,在嵌入式领域C++将会有辉煌的前景。对C++进行改造,使其适用于嵌入式环境,提高其可靠性,对于推动C++在嵌入式领域的应用是很重要的。MISRA-C已经在嵌入式C语言上取得了很大的成功,成为行业普遍认同和遵循的规范。我们希望MISRA-C++也能和MISRA-C一样,推动C++在嵌入式领域的规范化。

发表评论

登录后才能评论