检测和处理错误
本节阅读量:上一节介绍了新C++程序员在该语言中遇到的许多常见的语义错误。如果只是误用语言功能或代码逻辑错误,则错误可以比较简单的更正。
但程序中的大多数错误并不是由于无意中误用语言功能而发生的——相反,大多数错误是由于程序员的错误假设和/或缺乏适当的错误检测/处理而发生的。
例如,在设计用于查找学生成绩的函数中,您可能假设:
- 被查找的学生将存在。
- 所有学生的姓名都是唯一的。
- 使用字母评分(而不是及格/不及格)。
如果这些假设中的任何一个都不正确怎么办?如果没有预料到这些情况,程序可能会发生故障或崩溃(通常在未来的某个时间点,在编写函数之后)。
这种假设错误通常发生在三个关键位置:
- 当被调用函数返回时,程序员可能假设它是成功的,但其实它失败了。
- 当程序接收到输入(来自用户或文件)时,程序员可能会假设输入的格式正确。
- 调用函数时,程序员可能会假设传递参数在语义上是有效的,但实际上它们不是。
许多新程序员编写代码,然后只测试执行那些显而易见正确的代码路径。但其实也应该测试其它容易出错的路径。
在本课中,我们将讨论函数内部的错误处理策略(出错时该怎么办)。在后面的课程中,我们将讨论验证用户输入,然后介绍一个有用的工具来帮助记录和验证假设。
处理函数中的错误
函数可能会由于许多原因而失败——调用方可能传入了一个具有无效值的参数,或者函数体中的某些内容可能会失败。例如,如果找不到文件,则打开文件进行读取的函数可能会失败。
当这种情况发生时,您有相当多的选项可供选择。没有一个固定最好的方法来处理错误,这实际上取决于问题的性质以及问题是否可以修复。
有4种通用策略可以使用:
- 处理函数中的错误
- 将错误传递回调用方以处理
- 停止程序
- 抛出异常
处理函数中的错误
如果可能,最好的策略是从发生错误的同一函数中进行错误恢复,以便可以在不影响函数外部的任何代码的情况下更正错误。这里有两个选项:重试直到成功,或取消正在执行的操作。
如果错误是由于程序无法控制的原因而发生的,则程序可以重试,直到成功。例如,如果程序需要互联网连接,而用户已失去连接,则可以显示警告,然后使用循环定期重新检查互联网连接。或者,如果用户输入了无效的输入,程序可以要求用户重试,并循环,直到用户成功输入有效的输入。
另一种策略是忽略错误和/或取消操作。例如:
|
|
在上面的例子中,如果用户为y传递了无效的值,我们就忽略打印除法运算结果的请求。这样做的主要问题是,调用方或用户无法识别出问题。在这种情况下,打印错误消息可能会有所帮助:
|
|
然而,如果调用函数期望被调用函数产生返回值或一些有用的副作用,那么忽略错误可能不是一个可选项。
将错误传回调用方
在许多情况下,错误不能在检测错误的函数中合理地处理。例如,考虑以下函数:
|
|
如果y是0,我们应该怎么做?我们不能跳过程序逻辑,因为函数需要返回一些值。也不应该要求用户输入y的新值,因为这是一个计算函数,让用户输入的逻辑可能在其它函数中。
在这种情况下,最好的选择是将错误传递回调用方,希望调用方能够处理它。
如何才能做到这一点?
如果函数具有void返回类型,则可以将其更改为返回指示成功或失败的布尔值。例如,如果之前这样:
|
|
则可以更改为:
|
|
这样,调用方可以检查返回值,以查看函数是否因某种原因而失败。
如果函数返回正常值,则情况会稍微复杂一些。在某些情况下,不使用完整的返回值范围。在这种情况下,可以使用通常不可能发生的返回值来指示错误。例如,考虑以下函数:
|
|
某个数x的倒数定义为1/x,一个数乘以它的倒数等于1。
然而,如果用户调用reciprocal(0),会发生什么情况?我们得到一个除以零的错误和一个程序崩溃,所以很明显,应该防止这种情况。但这个函数必须返回一个double值,所以应该返回什么值?很明显,该函数永远不会产生0.0作为合法结果,因此可以返回0.0来指示错误情况。
|
|
然而,如果需要完整的返回值范围,则不可能使用返回值来指示错误(因为调用程序将无法区分返回值是有效值还是错误值)。
注释
在这种情况下,返回std::optional将是一个不错的选择。
致命错误
如果错误严重到程序无法继续正常运行,则这称为不可恢复错误(也称为致命错误, fatal error)。在这种情况下,最好的做法是终止程序。如果错误点在main()函数中,最好的办法是直接return一个非零值。然而,如果深入到某个嵌套子函数中,将错误传播回main()则不太可能。在这种情况下,可以使用退出语句( 例如std::exit() )。
例如:
|
|
Exceptions (异常)
由于将错误从函数返回给调用者是复杂的(许多不同的方法都会导致不一致,而不一致会导致错误),C++提供了一种完全独立的方法来将错误传递回调用者: exceptions。
基本思想是,当错误发生时,会“抛出”异常。如果当前函数没有“捕获”错误,则函数的调用方有机会捕获该错误。如果调用者没有捕获错误,则调用者的调用者有机会捕获错误。错误在调用堆栈中逐渐向上移动,直到捕获并处理它(此时执行正常继续),或者直到main()函数中也无法处理错误(此时程序因异常错误而终止)。
在后续章节介绍异常处理。
何时使用std::cout vs std::cerr vs logging
您可能想知道什么时候应该使用std::cerr vs std::cout vs logging到文本文件。
默认情况下,std::cout和std::cerr都将文本打印到控制台。然而,现代操作系统提供了一种将输出流重定向到文件的方法,以便可以捕获输出到文件中以供以后审查或自动处理。
对于此讨论,区分两种类型的应用程序是有用的:
- 交互式应用程序,用户在运行后将与之交互的应用程序。大多数独立应用程序,如游戏和音乐应用程序,都属于这一类。
- 非交互式应用程序,是不需要用户交互才能运行的应用程序。这些程序的输出可以用作其他应用的输入
在非交互式应用程序中,有两种类型:
- 工具类,通常是为了产生某些即时结果而启动,然后在产生这样的结果后终止。这方面的一个例子是Unix的grep命令,这是一个实用程序,用于搜索文本中与某种模式匹配的行。
- 服务类,通常在后台静默运行以执行某些功能。这方面的一个例子是病毒扫描程序。
这里有一些经验法则:
- 面向用户的文本输出结果使用std::cout。
- 对于交互式程序,请将std::cout用于普通的面向用户的错误消息(例如,“您的输入无效”)。使用std::cerr或日志文件来记录状态和诊断信息,这些信息可能有助于诊断问题,但对于普通用户来说可能并不有趣。这可以包括技术警告和错误(例如,函数x的输入错误)、状态更新(例如,成功打开文件x,未能连接到互联网服务x)、长任务的完成百分比(例如,格式转换完成50%)等…
- 对于非交互式程序(工具或服务),仅将std::cerr用于错误输出(例如,无法打开文件x)。这允许错误与正常输出分开显示或分析。
- 对于本质上是事务性的任何应用程序类型(例如,处理特定事件的应用程序类型,如交互式web浏览器或非交互式web服务器),使用日志文件生成事件的事务日志,这些事件可以在以后查看。例如,输出当前正在处理的文件、完成百分比、开始计算的某个阶段的时间戳、警告和错误消息。
