C++编程规范整理(二)

异常和错误

  1. 异常处理什么

    异常就是Bug、失败等不符合程序语义的行为。几乎所有的易用的语言,如Java、PHP、C#都有exception类和try…catch结构来处理异常。如果正确地使用异常,异常可以使程序的正常处理逻辑和异常处理逻辑分离,使得程序逻辑更加清晰,可读性更高,更容易维护。从而极大降低了代码测试、跟踪、调试的难度。但是如果使用不当,轻则降低程序运行效率,重则使程序难以维护。

    使用异常来处理程序中可能出现的错误,有以下几个优点:

    • 异常不能不加修改的忽略
      • 处理错误码需要显示的编写更多的代码来判断异常情况,而异常则可以直接通过try…catch结构来捕捉,然后分离处理逻辑。
    • 异常时自动传播的
      • 错误码需要通过返回值等方式进行手工传播,很容易被遗漏。而异常只要没有被catch住,则会一直沿着调用栈传播。
    • 异常能使正常处理流程与错误处理流程截然分开
      • 异常的错误处理、错误恢复代码都在catch块中,一目了然。
    • 构造函数、析构函数和运算符重载只能使用异常报告错误
      • 构造函数、析构函数没有返回值,运算符重载函数的返回值往往有特殊规定。因此这些函数无法返回错误码来由上层来处理错误。只能通过异常来处理。
    • 异常是自说明的
      • 在catch块中处理错误时,能很轻松地得到文件名、类名、函数名、行号、栈信息、错误信息等数据。能够充分说明发生错误时,程序所处的状态。
      • 不同的程序库往往都会定义自己的一套错误码。如此可能导致错误处理的逻辑混乱。
    • 高效
      • 有人担心异常会导致程序性能下降。但经测试。一个单线程程序每秒能抛出25万条异常(无栈跟踪信息),因此异常对性能的影响是非常小的。

    介于以上优点,我们推荐使用异常来处理程序中可能发生的错误。但是使用过程中需要注意的是:

    • 异常只用于报告程序逻辑上不应该出现的错误
    • 绝不是用异常来作"快捷跳转"(这一点C++程序员需要格外注意,很多人拿异常当goto用)
    • 如果异常被捕捉,需要使用一定的日志机制来记录,以便于追踪问题

    适用异常处理的例子

    1. 参数不合法
    2. 空指针
    3. 内存不足
    4. 文本可是错误,不能解析

    不适用异常处理的例子

    1. 遍历到容器的末端
    2. 容器查找操作没有结果
    3. 在很深的递归里成功完成了任务,直接跳回(绝对禁止)
  2. 异常类的单根性

    异常是接口的重要组成部分,为确保接口的统一性和使用上的方便,需要保证同一库抛出的异常具有共同的祖先类(如Exception)。另外在追查问题、协助调试上,具有统一的祖先类也方便于设计统一而丰富的接口来控制日志输出。

    因此,在确保异常类的单根性上,需要保证:

    1. 同一库抛出的异常类型有共同的基类,该基类应该至少提供以下接口:
      1. 提供一个name()方法,返回异常的类型;
      2. 提供一个what()方法,返回自定义的错误信息;
      3. 提供file()、line()、function()方法,返回异常被抛出的文件、行号、函数信息;
      4. 提供stack()方法,返回栈跟踪信息
    2. 如果库使用了标准库或其它会抛异常的第三方库,而它们抛出的异常会破坏异常的单根性或不具有上述接口时,强烈建议该库接住这些异常,包装后重新抛出
  3. 确保接住所有异常,并通过打印日志记录异常

    如果使用了异常机制,那么被接住的异常是编程、调试、跟踪的利器,而没有被接住的异常往往是程序员的灾难。在异常中,有些异常是符合预期的,有些异常却是绝对的逻辑或者代码的错误,作为有足够健壮性的代码,需要能够同时处理这两种可能的情况。在捕捉到的异常后,要完整又有效得表述异常信息,并记入在日志中以便追查问题。

    通常,程序都会有一个总控函数(如main或者tread_main等),最好在这些函数里接住所有异常,并打印相关日志,以免漏掉异常的记录和处理。

    另外错误处理方式(错误码、异常等)是函数接口的最重要的组成部分之一。由于难以在修改错误处理方式时保证修改所有的调用函数,同时编译器不能对此作出任何检查。因此,正如代码重构不能把不返回错误码的函数改为返回错误码一样,不能把不抛出异常的函数改为抛出异常的函数。

    因此,在使用异常机制时,需要保证:

    1. 确保接住所有异常,强烈建议接住std::Exception、…和第三方库的异常祖先类这几种异常
    2. 确保所有异常都能够正确地记录日志
    3. 进行代码重构时,强烈建议不使原本不抛异常的公有函数抛出异常。若使用了会抛异常的库,强烈建议保持所有异常,并以原接口定义的方式报告错误
  4. 异常的抛出和截获

    按传值的方式接住异常可能会导致异常对象被截断,从而失去正确的异常信息。

    另外无参数的throw语句表示重新抛出异常的意思。由于异常类型是静态绑定的,因此除非需要改变异常的信息,不然不应该使用参数来重新抛出异常。

    1. 强烈建议使用传值方式抛出异常、使用传引用方式截获异常
    2. 推荐使用无参数 throw 语句重新抛出语句
  5. 异常规格说明

    异常规格说明是指声明函数可能抛出的异常。它明确描述了一个函数可以抛出什么样的异常,使代码更容易被理解。编译器在编译阶段有时能够检测到异常的不一致;而且在运行时,如果函数抛出了一个不在异常规格返回内的异常,则系统能够检测到该问题,然后unexpected函数会被自动调用,并处理该异常。但是这个处理过程会有很多问题:

    1. 在运行时检查,会增加函数的运行时开销
    2. 程序违反异常规格说明时,unexpected的调用顺序为:terminate->abort,然后直接导致程序退出
    3. 当函数中调用了其他接口或者函数时,往往无法预知会抛出什么异常,结合第二条,这将导致程序的最终行为不同于预期

    因此,强烈建议不使用异常规格说明,除非不得已。

  6. 错误码

    提及错误码这个概念,是否会觉得很原始,尤其是在这个异常横行的年代。但是错误码还是非常有用的,

    1. 当必须与C语言兼容时
    2. 当"错误"是可预期的状态而不是真正的错误时
    3. 当需要网络或其他通讯手段来传播状态信息时(模块对网络请求的响应,它是无法通过异常传播的)
    4. 当使用异常可能会造成接口不统一时

    错误码就闪亮登场了。

    当然当使用错误码时,也有需要特别关注的地方:

    1. 当函数调用了返回错误码的函数时,必须检查其返回值(由于错误码检查会大大增加代码量,很多程序员常常忽略对"不大可能"出错的函数的错误码的检查,造成错误被掩盖,从而造成了严重而难以追查的错误。)
    2. 错误码原则上以0表示成功,其它非0值表示错误(UNIX程序的一贯风格)
    3. 错误码应尽可能详细,并能说明错误发生的原因,不应该一律使用-1
    4. 避免混合设计返回错误码与返回布尔值的函数(与第二条相违背)
    5. 一个库使用的错误码必须统一,并且使用enum定义(便于维护、便于编译器发现混用不同类型错误码的情况)
    6. 错误码枚举值名称必须富有意义,不应使用不常见的缩写
    7. 必须设计一个函数,把错误码翻译成字符串,以方便打log 与 debug
    8. 禁止错误码与返回值混用
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值