是否应该检查C中的每个小错误?


60

作为一名优秀的程序员,应该编写能处理其程序每个结果的强大代码。但是,当发生错误时,C库中的几乎所有函数都将返回0或-1或NULL。

有时很明显,需要进行错误检查,例如,当您尝试打开文件时。但是我经常忽略诸如printf或什至malloc因为我认为没有必要而进行错误检查。

if(fprintf(stderr, "%s", errMsg) < 0){
    perror("An error occurred while displaying the previous error.");
    exit(1);
}

仅忽略某些错误是一种好习惯,还是有一种更好的方法来处理所有错误?


13
取决于您正在处理的项目所需的鲁棒性级别。有可能接收来自不受信任方(例如,面向公众的服务器)的输入或在不完全受信任的环境中运行的系统,需要非常谨慎地进行编码,以避免代码成为定时炸弹(或削弱最弱的链接) )。显然,业余爱好和学习项目不需要这种鲁棒性。
rwong 2015年

1
某些语言提供例外。如果您没有捕获异常,则线程将终止,这比让它以错误状态继续运行要好。如果选择捕获异常,则可以用一条try语句覆盖许多行中的许多错误,包括被调用的函数和方法,因此您不必检查每个调用或操作。(还要注意,某些语言在检测简单错误(例如空解除引用或数组索引超出范围)方面比其他语言更好。)
Erik Eidt 2015年

1
问题主要是方法论上的问题:通常在您仍然想弄清楚应该实现的目标时就不编写错误检查,并且由于要获得“快乐之路” 而不想立即添加错误检查首先。但是您仍然应该检查malloc和co。不必跳过错误检查步骤,而是必须对编码活动进行优先级排序,并使错误检查成为TODO列表中的隐式永久性重构步骤,只要您对代码感到满意,就可以应用。添加可放大的“ / * CHECK * /”注释可能会有所帮助。
coredump

7
我不敢相信没有人提到errno!如果您不熟悉,虽然确实是“ C库中的几乎所有函数都将返回0或−1或NULL发生错误时”,但它们也会设置全局errno变量,您可以使用进行访问#include <errno.h>,然后简单地读取的价值errno。因此,例如,如果open(2)返回-1,则您可能需要检查是errno == EACCES,这将指示权限错误,还是ENOENT,这将指示请求的文件不存在。
wchargin

1
@ErikEidt C不支持try/ catch,尽管您可以通过跳转来模拟它。
2015年

Answers:


29

通常,代码应在适当的地方处理特殊情况。是的,这是一个模糊的陈述。

在具有软件异常处理功能的高级语言中,这通常表示为“在实际上可以对其执行某些操作的方法中捕获异常”。如果发生文件错误,也许您可​​以让它在堆栈中冒泡,直至UI代码,该代码实际上可以告诉用户“您的文件无法保存到磁盘”。异常机制有效地吞噬了“每一个小错误”,并在适当的位置隐式地处理它。

在C语言中,您没有那么奢侈。有几种处理错误的方法,其中一些是语言/库功能,有些是编码实践。

仅忽略某些错误是一种好习惯,还是有一种更好的方法来处理所有错误?

忽略某些错误?也许。例如,可以合理地假设写入标准输出不会失败。如果失败了,您怎么会告诉用户?是的,最好忽略某些错误,或者防御性地编写代码以防止出现这些错误。例如,在除法之前检查零。

有一些方法可以处理所有或至少大多数错误:

  1. 您可以使用跳转(类似于gotos)进行错误处理。尽管在软件专业人员中引起争议,但对于他们而言,它们还是有有效用途的,尤其是在嵌入式和对性能至关重要的代码(例如Linux内核)中。
  1. 级联ifs:

    if (!<something>) {
      printf("oh no 1!");
      return;
    }
    if (!<something else>) {
      printf("oh no 2!");
      return;
    }
    
  2. 测试第一个条件,例如打开或创建文件,然后假定后续操作成功。

健壮的代码是好的,应该检查并处理错误。哪种方法最适合您的代码,取决于代码的功能,故障的严重程度等,只有您才能真正回答。但是,这些方法经过了实战测试,并在各种开源项目中使用,您可以在其中查看实际代码如何检查错误。


21
抱歉,未成年人请在此处选择-“写入标准输出不会失败。如果失败,无论如何您将如何告诉用户?” -通过写标准错误?无法保证无法写入其中一个意味着无法写入另一个。
Damien_The_Unbeliever 2015年

4
@Damien_The_Unbeliever-特别是因为stdout可以重定向到文件,或者甚至不存在,具体取决于系统。stdout不是“写入控制台”流,通常是这样,但不一定如此。
DavorŽdralo2015年

3
@JAB您向stdout... 发送了一条消息……很明显:-)
TripeHound

7
@JAB:如果合适的话,您可以退出EX_IOERR左右。
John Marshall

6
无论您做什么,都不要像没有任何反应一样继续运行!如果命令 dump_database > backups/db_dump.txt在任何给定点都无法写入标准输出,则我不希望它继续进行并成功退出。(并不是说数据库是以这种方式备份的,但重点仍然存在)
Ben S

15

但是,当发生错误时,C库中的几乎所有函数都将返回0或-1或NULL。

是的,但是您知道您调用了哪个函数,不是吗?

实际上,您有很多信息可以放入错误消息中。您知道调用了哪个函数,调用了该函数的名称,传递了哪些参数以及该函数的返回值。对于非常有用的错误消息,这是足够的信息。

您不必为每个函数调用都执行此操作。但是,当您真正需要的是有用的信息时,第一次看到错误消息“显示上一个错误时发生了错误”,这将是您最后一次看到该错误消息,因为您将立即进行更改错误消息,提示您可以帮助您解决问题。


5
这个答案让我微笑,因为它是真的,但没有回答问题。
RubberDuck

它怎么不回答这个问题?
罗伯特·哈维

2
我认为C(和其他语言)混淆了布尔值1和0的一个原因是,任何返回错误代码的体面函数都返回“ 0”而没有错误的原因相同。但是然后你不得不说if (!myfunc()) {/*proceed*/}。0表示没有错误,任何非零的内容都是某种错误,每种错误都有自己的非零代码。我的一个朋友所说的话是“只有一个真理,但有许多谎言”。在if()测试中,“ 0”应为“ true”,非零值应为“ false”。
罗伯特·布里斯托

1
不,按照您的方式操作,只有一个可能的负错误代码。以及许多可能的no_error代码。
罗伯特·布里斯托

1
@ robertbristow-johnson:然后将其读取为“没有错误”。 if(function()) { // do something }读为“如果函数正确执行,则执行某些操作。” 或者,甚至更好的是,“如果函数成功执行,则执行某些操作”。认真地说,那些了解公约的人不会对此感到困惑。false发生错误时返回(或0)将不允许使用错误代码。 零在C语言中表示错误,因为这就是数学的工作原理。参见programs.stackexchange.com/q/198284
罗伯特·哈维

11

TLDR;您几乎永远都不要忽略错误。

C语言缺乏良好的错误处理功能,每个库开发人员都无法实施自己的解决方案。更多现代语言具有内置的异常,这使得处理此特定问题变得更加容易。

但是,当您陷入C困境时,您就不会有这样的特权。不幸的是,每次调用一个函数时,您极有可能失败,而您仅需为此付出代价。否则,您将遭受更严重的后果,例如无意间覆盖内存中的数据。因此,作为一般规则,您必须始终检查错误。

如果您不检查返回的情况,fprintf很可能会留下一个错误,在最佳情况下,它不会达到用户期望的效果,更糟糕的情况是,在飞行过程中会爆炸整个问题。没有任何借口破坏自己。

但是,作为C开发人员,使代码易于维护也是您的工作。因此,有时为了且仅当)错误不会对应用程序的整体行为造成任何威胁时,为了清楚起见,您可以放心地忽略它们。

这样做是同样的问题:

try
{
    run();
} catch (Exception) {
    // comments expected here!!!
}

如果您发现在空白catch块内没有好的注释,那肯定是一个问题。没有理由认为对的调用malloc()将在100%的时间成功执行。


我同意可维护性是一个非常重要的方面,这一段确实回答了我的问题。感谢您的详细回答。
德里克·朕会功夫

我认为,当桌面程序仅使用几MB的内存时,成千上万的程序永远不会费心检查它们的malloc调用,却不会崩溃,这是一个懒惰的好理由。
whatsisname 2015年

5
@whatsisname:仅仅因为它们不会崩溃并不意味着它们没有默默地破坏数据。malloc()是您肯定要检查返回值的函数!
TMN 2015年

1
@TMN:如果malloc失败,程序将立即进行段错误并终止,它将不会继续执行操作。这都是关于风险和适当的问题,而不是“几乎从不”。
whatsisname 2015年

2
@Alex C不缺少良好的错误处理。如果需要例外,可以使用它们。不使用异常的标准库是一个有意的选择(异常会迫使您进行自动内存管理,而对于低级代码,通常您不希望这样做)。
martinkunev

9

这个问题实际上不是特定于语言的,而是特定于用户的。从用户角度考虑问题。用户执行某些操作,例如在命令行上键入程序的名称并按Enter。用户期望什么?他们怎么知道出了什么问题?如果发生错误,他们可以负担得起吗?

在许多类型的代码中,这种检查是多余的。但是,在诸如核反应堆的安全可靠性高的关键代码中,病理错误检查和计划的恢复路径是日常工作的一部分。花时间问“如果X失败会发生什么?我如何回到安全状态?”,这是值得的。使用不太可靠的代码(例如,用于视频游戏的代码),您可以省去更少的错误检查。

另一个类似的方法是,通过捕获错误,您实际上可以对状态进行多少改进?我不能指望骄傲地捕捉到异常的C ++程序的数​​量,仅是将它们抛出,因为它们实际上并不知道如何处理它们……但是他们知道他们应该进行异常处理。这样的程序从额外的努力中一无所获。仅添加您认为实际上可以更好地处理这种情况的错误检查代码,而不是仅不检查错误代码。话虽这么说,C ++对于处理在异常处理期间发生的异常具有特定的规则,以便以一种更有意义的方式捕获它们(通过这种方式,我的意思是调用Terminate()以确保您为自己建立的葬礼堆点亮了在适当的荣耀中)

您会如何病态?我开发了一个定义“ SafetyBool”的程序,仔细选择了它的真值和假值,使其平均分配1和0,并且选择它们的目的是使许多硬件故障(单位翻转,数据总线)中的任何一个痕迹损坏等)不会导致布尔值被误解。不用说,我不会声称这是在任何旧程序中使用的通用编程实践。


6
  • 不同的安全要求要求不同的正确性。在航空或汽车控制软件中,将检查所有返回值,请参见。MISRA.FUNC.UNUSEDRET。快速概念验证,它永远不会离开您的机器。
  • 编码会花费时间。在非安全关键型软件中进行过多无关的检查最好在其他地方花费。但是质量和成本之间的最佳结合点在哪里?这取决于调试工具和软件复杂性。
  • 错误处理可能会使控制流模糊,并引入新的错误。我非常喜欢Richard“ network” Stevens的包装器功能,该功能至少会报告错误。
  • 错误处理很少会成为性能问题。但是大多数C库调用将花费很长时间,以至于检查返回值的成本非常小。

3

这个问题有点抽象。而且不一定是C语言。

对于较大的程序,您将有一个抽象层。也许是引擎,库或框架中的一部分。这层就不会在乎天气,你得到一个有效的数据或输出将是一些默认值:0-1null等。

然后有一个层将作为您与抽象层的接口,它将执行许多错误处理以及可能的其他操作,例如依赖项注入,事件侦听等。

之后,您将拥有具体的实现层,您可以在其中实际设置规则并处理输出。

因此,我的观点是,有时最好将一部分代码中的错误处理完全排除在外,因为该部分根本无法完成该工作。然后有一些处理器可以评估输出并指出错误。

这样做主要是为了分开职责,以提高代码的可读性和更好的可伸缩性。


0

通常,除非您有充分的理由检查该错误情况,否则应该这样做。我唯一不检查错误条件的好理由是,如果失败,您将无法做有意义的事情。不过,这是一个很难满足的标准,因为始终有一个可行的选择:优雅地退出。


我通常不同意您的看法,因为错误是您没有考虑到的情况。如果您不知道错误在什么情况下突然出现,那么很难知道该错误将如何显示。从而在实际处理数据之前进行错误处理。
Creative Magic

就像我说的那样,如果某件事返回或引发错误,即使您不知道如何解决错误并继续操作,也可以始终正常地失败,记录错误,告诉用户它没有按预期工作,等等。重点是该错误不应被忽略,未记录,“静默失败”或使应用程序崩溃。这就是“优雅失败”或“优雅退出”的含义。
聪明的新词
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.