C++ 编程规范 7 异常与错误处理

来源:互联网 发布:python datetime 时区 编辑:程序博客网 时间:2024/06/05 09:19

7 异常与错误处理

  7.1  异常

异常是C++语言的一个强大特性,在正确使用之前需要深入了解,以及使用异常代码的上下文。

原则7.1 减少不必要的异常

说明:异常对编码技能要求更高,使用中容易出错,首先从安全性角度考虑,尽量少用或者不用异常。

相比返回错误,异常的优点:
 异常可以集中捕捉,错误检测与算法处理相分离,算法逻辑更清晰;而返回错误在每个返回点都

    要进行检测与错误处理,代码逻辑分散。

 异常的约束更强,用户不能忽略抛出的异常,否则程序默认会被终止,而返回错误则可能被忽略。

异常的缺点也很明显:

 必须检查所有调用点是否可能抛出异常,在抛出后必须正确处理状态和资源变量等,否则可能导

    致对象状态不正确或者资源泄露等。例如:如果f()依次调用了g()和h(),h抛出被f捕获的异常,

    g就要当心了,避免资源泄露。

 必须清楚可能抛出的所有异常,并在合适的地方捕捉,如果遗漏通常会导致程序被终止。

 使用异常很难评估程序的控制流,代码很难调试。

  目标文件变大,编译时间延长,性能下降。

若对异常缺乏充分理解,可能会在不恰当的时候抛出异常, 或在不安全的地方从异常中恢复。

适用异常的几个场景:

 出现“不应该出现的”失败,且不能被忽略必须处理,比如分配内存失败。

 上层应用决定如何处理在底层嵌套函数中  “不可能出现的”失败。

 错误码难以通过函数的返回值或参数返回,比如流。

 许多第三方C++库使用异常,必须在系统边界与第三方C++库结合处使用异常便于跟这些库集成。

 在测试框架中使用异常很方便。

规则7.1 构造和析构函数不能抛出异常

说明:如果构造和析构函数执行失败则无法安全地撤销和回滚,故这些函数不能向外抛出异常。

为了降低复杂性,建议在这类函数中实现最简单的逻辑。

规则7.2 通过传值的方式抛出,通过引用的方式捕获

说明:抛出异常时,如果抛出指针,谁释放指针就成为问题。捕捉时如果是传值,会存在拷贝,拷贝可

能不成功(比如异常是由于内存耗尽造成的),而且拷贝得不到派生类对象,因为在拷贝时,派生类对象

会被切片成为基类对象。

规则7.3 确保抛出的异常一定能被捕捉到

说明:异常未被捕捉到,系统的默认行为是终止程序运行,所以要确保程序产生的异常都能被捕捉。

规则7.4 确保异常发生后资源不泄漏

说明:异常发生后,当前代码执行序列被打断,需要查看分配的内存、文件和内核句柄等资源是否正确

释放,避免资源泄漏,尤其每个可能的返回点是否正确释放资源。

示例:如下代码存在内存泄漏

    int PortalTransformer::transRLS
    {
       RLS_Service* service = NULL;
       NEW( service, RLS_Service );
       parser->adoptDocument();//失败时会抛异常
        //....
       delete service;
       service =NULL;
       return 0;
   }
调用adoptDocument出现的异常没有在函数transRLS里面被捕获,而是在父函数里面捕获了异常的派生

类。如果发生异常,则NEW( service, RLS_Service )分配的内存泄漏。

解决方案:在函数transRLS里面捕获adoptDocument的异常,如果发生异常,则删除指针service 。

规则7.5 独立编译模块或子系统的外部接口禁止抛异常

说明:异常处理没有普遍通用的二进制标准,所以不允许跨模块抛异常。

  7.2  错误处理策略

原则7.2 建立合理的错误处理策略

说明:这里所说的错误指运行时错误,并非模块内部的编程和设计错误。模块内部的编程和设计错误

应该通过断言标记。

在设计早期确定错误处理策略,包括:鉴别,严重程度,错误检查,错误处理,错误传递,错误报告

方案。

错误鉴别:对每个实体(函数、类、模块),记录该实体内部和外部的不变式、前置条件、后置条件以

及它支持的错误安全性保证。

错误严重程度:对于每个错误,标明严重级别。

错误检查:对于每个错误,记载哪些代码负责检查它。

错误处理:对于每个错误,标明负责处理它的代码。

错误报告:对于每个错误,标明合适的报告方法。

错误传递:对每个模块,标明使用什么编程机制传递错误,如C++异常、CORBA异常、返回值。

错误处理策略应该只在模块边界改变。如果模块内外所使用的策略不同,则所有模块入口函数都要直

接负责由内到外的策略转换。例如,在一个内部使用C++异常,但提供C语言的API边界的模块中,所有

C语言的API必须用catch(„)捕获所有异常并将其转换为错误代码。

原则7.3 离错误最近的地方处理错误或转换错误

说明:当函数检查到一个自己无法解决的错误,而且会使函数无法继续执行的时候,就应该报告错误。

如果缺乏处理的上下文,应该向上传播错误。

规则7.6 错误发生时,至少确保符合基本保证;对于事务处理,至少符合强保证;对于原子操作,符

合无错误保证

说明:基本保证是指访问对象时的状态都是正确的;强保证是对基本保证的增强,不仅要状态正确,

而且当失败时状态要回滚到操作前的状态,要么成功要么什么都不做;无错误保证是不能出现失败。

编码中严格遵循此原则,会极大提升程序的健壮性。

符合基本保证的代码示例:如下代码是解析输入流到对象,流抛出异常方式呈报错误

    void CMessage::Parse(IStream* input)
    {
       try
       {
          m_uiMessageLen = input.ReadInteger();//失败会抛出异常
         if (0 == m_uiMessageLen || m_uiMessageLen>MAX_MESSAGE_LEN)
           {
              throw invalid_argument("Invalid message len in CMessage::Parse ");
           }
          m_pMessage = new char[m_uiMessageLen];//失败抛出异常
           input.Read(m_pMessage, m_uiMessageLen);//失败抛出异常
           //.....
        }
       catch (const exception &exp){
           ResetContent();//把对象的所有字段都设置为无效
           throw exp;
        }
       return;
    }
上例确保对象字段的值要么都有效要么都无效,不会出现部分有效部分无效的情况,否则必须处理异

常被抛出时的对象状态,给调用者带来麻烦,容易遗漏。故接口应该至少符合基本保证。

可以把该函数改造为符合强保证,如下:

    void CMessage::Parse(IStream* input)
    {
       CMessage temp;
       try
        {
           temp .m_uiMessageLen = input.ReadInteger();//失败会抛出异常
           if (0 == temp .m_uiMessageLen || temp .m_uiMessageLen>MAX_MESSAGE_LEN)
           {
              throw invalid_argument("Invalid messageLen in CMessage::Parse ");
           }
           temp .m_pMessage = new char[temp .m_uiMessageLen];//失败抛出异常
           input.Read(temp .m_pMessage, temp .m_uiMessageLen);//失败抛出异常
           //.....
        }
       catch (const exception &exp){
           temp .ResetContent();//把对象的所有字段都设置为无效
           throw exp;
        }
       swap(temp);//成功后执行swap
       return;
    }
失败后,对象的状态不受影响;而成功后,执行的swap必须符合无错误保证,否则此函数是无法支持

强保证的。

原创粉丝点击