何时以及如何使用异常处理?

90

我正在阅读有关异常处理的内容。我已经了解了什么是异常处理,但是还有一些问题:

  1. 什么时候抛出异常?
  2. 除了抛出异常,我们是否可以使用返回值来指示错误?
  3. 如果我将所有函数都用try-catch块保护起来,这不会降低性能吗?
  4. 何时使用异常处理?
  5. 我看到一个项目中每个函数都包含一个try-catch块(即整个函数的代码都被try-catch块包围)。这是一个好的实践吗?
  6. try-catch和__try __except之间的区别是什么?

https://dev59.com/SFHTa4cB1Zd3GeqPTahr - cpx
你需要提出更具体的问题,而不是仅仅问“何时抛出异常”。当发生异常情况时,你才会抛出异常。 - Falmarri
我曾经写过关于PHP的相关内容,但我认为几乎所有的东西都适用于C++。请查看博客文章 - ircmaxell
7个回答

111

这是一份相当全面的关于异常处理指南,我认为它是必读的:

异常和错误处理 - C++ FAQ 或者 C++ FAQ lite

通常情况下,当你的程序可以识别一个阻止执行的外部问题时,抛出异常。如果从服务器接收到无效数据,则抛出异常。磁盘空间不足?抛出异常。宇宙射线阻止你查询数据库?抛出异常。但如果你从自己的程序内部获得了一些无效数据,就不要抛出异常。如果你的问题来自于自己糟糕的代码,最好使用ASSERT来防范。异常处理是为了识别程序无法处理的问题并告诉用户,因为用户可以处理它们。但是程序中的错误不是用户能够处理的,因此程序崩溃并不比"answer_to_life_and_universe_and_everything的值不是42!这不应该发生!!!11"异常少多少。

在你能够有所作为的地方捕获异常,比如显示一个消息框。我更喜欢在某个处理用户输入的函数中捕获一次异常。例如,用户按下"Annihilate all hunams"按钮,在annihilateAllHunamsClicked()函数内部有一个try...catch块来告诉用户"我不能这样做"。即使消灭人类是一个需要调用数十个函数的复杂操作,只有一个try...catch块,因为对于用户来说它是一个原子操作 - 一个按钮点击。每个函数内的异常检查是冗余而且难看的。

此外,我强烈建议熟悉RAII - 即确保所有初始化的数据都可以自动销毁。这可以通过尽可能在堆栈上初始化,以及在需要在堆上初始化时使用某种智能指针来实现。在堆栈上初始化的所有内容都将在抛出异常时自动销毁。如果使用C风格的愚笨指针,则在抛出异常时有可能导致内存泄漏,因为没有人可以在异常时清理它们(当然,您可以将C风格指针用作类成员,但请确保在析构函数中处理它们)。


2
感谢您宝贵的意见。“异常处理是必要的,以便识别程序无法处理的问题并告知用户,因为用户可以处理它们。”我可以返回错误值而不是抛出异常,在调用函数中检查错误并显示一条消息,这将帮助用户处理它。 - Umesha MS
请再次查看常见问题解答,特别是本节中第三和第四个代码片段:http://www.parashift.com/c++-faq-lite/exceptions.html#faq-17.2 异常很棒,因为它们可以让您的错误从任何调用堆栈深度浮出水面... 就像如果错误代码是深潜器,那么异常就是潜艇 :) - Septagram
2
如果你的问题源于自己的糟糕代码,最好使用ASSERT来防范。ASSERT不会让你继续执行与错误分支无关的任务(仅因为程序在一个函数中有一个错误并不意味着它不能做其他有用的事情)。它不会让你使用自定义日志记录器。它将绕过使用RAII获得的析构函数的好处(想要在程序终止之前刷新文件缓冲区吗?最好不要跳过那些fclose析构函数!)。它也不会给你完整的堆栈跟踪。 - user4266696
关于RAII和“愚蠢指针”,有一些需要说的话。仅仅因为你使用了“愚蠢指针”并不意味着你没有遵循RAII。请记住,只有在分配系统资源时才需要确保它们被释放。指针只是堆栈上保存内存地址的变量。使用“愚蠢指针”并不需要对象分配和初始化。一个“愚蠢指针”可以简单地保存在其他地方分配的对象的地址,并且完全可以使用。(续在下一条评论中) - SeanRamey
你可能有一个Renderer对象,它存储了Display对象的地址指针,以便将物体绘制到Display上,但是你不能使用智能指针,因为智能指针会在销毁Renderer对象时销毁Display对象,这不是你想要的结果。在这种情况下,只能使用“dumb pointer”或引用。 - SeanRamey

13

异常在各种情况下都非常有用。

首先,有些函数计算前置条件的成本太高了,如果发现前置条件不满足,最好只是计算并通过抛出异常中止操作。例如,您无法反转奇异矩阵,但是为了计算它是否奇异,需要计算行列式,这非常昂贵:它可能必须在函数内部完成,因此只需“尝试”反转矩阵,并在无法反转时通过抛出异常报错。这基本上是使用 负前置条件异常

然后还有其他情况,其中您的代码已经很复杂,并且将错误信息传递到调用链上很困难。这部分原因是 C 和 C++ 的数据结构模型已经损坏:有其他更好的方式,但 C++ 不支持它们(例如,在 Haskell 中使用单子)。这种用法基本上是 我不想正确处理,所以就抛出异常:这不是正确的方法,但这是实际的方法。

然后,异常的主要用途是报告外部前置条件或不变量,例如内存或磁盘空间等资源不足的情况。在这种情况下,通常会终止程序或其主要子部分,异常是传递有关问题信息的好方法。 C++ 异常设计用于报告阻止程序继续运行的错误

大多数现代语言使用的异常处理模型(包括 C++)被认为是不完善的。它的强大程度过于夸张。理论家们现在已经开发出了比完全开放的“抛出任何东西”和“可能也可能不捕获它”的模型更好的模型。此外,使用类型信息对异常进行分类并不是一个很好的想法。

因此,您最好只在实际错误时且没有其他处理方式时抛出异常,并尽可能靠近抛出点处捕获异常


16
请问您能否为“异常处理已知存在问题”和“理论家现在已经开发出更好的模型”添加一些链接/参考资料吗? - Juraj Blaho
1
在捕获异常时通常需要知道的一个关键问题,但大多数异常都没有提供有用的数据,那就是抛出异常的代码是否对系统状态产生了任何影响。理论家的模型中是否有任何处理这个问题的方法? - supercat
1
你可以抛出一个未被捕获的异常,但这是不好的。比goto更糟糕,因为goto至少总是会跳转到某个地方。静态类型无法处理异常规范:没有一个合理的系统可以使多态函数具有异常规范,因为它取决于类型变量的特定实例。请注意,这些已从新的C++标准中删除。新模型称为“分限延续”。:http://en.wikipedia.org/wiki/Delimited_continuation - Yttrill
顺便提一下:C++异常特别恶心,因为你可以抛出任何类型的值。与Ocaml异常相比,后者每个程序都有有限数量的声明异常构造函数。再与(我的语言)Felix进行比较,它允许非局部goto,这些goto在静态上保证有目标,然后允许将它们包装在闭包中并传递给计算函数。在这里,该函数会使用客户端提供的处理程序而不是自己发明的处理程序中止。就像您传递C++ catch子句一样,您抛出catch子句(而不是任意对象)。 - Yttrill
Felix计划的优点在于您无法“忘记”传递错误处理程序,因为它是函数签名的一部分,并且您无法抛出无法捕获的内容。它是静态安全的。它仍然不是非常令人满意(谁喜欢goto?),但它比将任意对象作为异常抛出要好得多。 - Yttrill
显示剩余4条评论

5
如果问题来自于你自己的糟糕代码,最好使用ASSERT进行保护。异常处理需要用于识别程序无法处理并告知用户的问题,因为用户可以处理它们。但是,程序中的错误不是用户可以处理的东西,所以程序崩溃并不能提供太多信息。
我不同意被接受的答案的这个方面。ASSERT并不总是比抛出异常更好。如果异常只适用于运行时错误(或“外部问题”),那么std::logic_error有什么作用呢?
逻辑错误几乎定义了防止程序继续的一种情况。如果程序是一个逻辑结构,并且发生了超出该逻辑域的条件,那么它怎么能继续呢?趁着还能输入,抛出一个异常吧!
这并不是没有先例。以std::vector为例,它抛出一个逻辑错误异常,即std::out_of_range。如果你使用标准库并且没有顶层处理程序来捕获标准异常——即使只是调用what()和exit(3)——那么你的程序就会突然而静默地终止。
ASSERT宏是一种较弱的保护。没有恢复机制。除非你没有运行调试版本,否则就没有执行。ASSERT宏属于计算速度比今天慢6个数量级的时代。如果你要费力测试逻辑错误,但在生产中不使用该测试,那么你最好对自己的代码有很多信心!
标准库提供了逻辑错误异常,并使用它们。它们之所以存在是有原因的:因为逻辑错误会发生,而且是异常情况。仅仅因为C特性具有断言并不是依赖这样一个原始(且可以说是无用)的机制的理由,当异常处理工作得更好时。

我不会仅仅因为标准库这么做就为其辩护。标准库中存在一些丑陋和不一致的东西。 - Sergey Kolesnik

3

最佳阅读材料

异常处理在过去的十五年中一直备受关注。然而,尽管对于如何正确处理异常存在普遍共识,但使用方式上仍然存在分歧。不正确的异常处理很容易被发现,也很容易避免,是一个简单的代码(和开发者)质量指标。我知道绝对规则会显得固执或夸张,但通常情况下你不应该使用try/catch。

http://codebetter.com/karlseguin/2010/01/25/don-t-use-try-catch/


3
这篇文章的意思是不要使用try/catch{}吗? - Craig McQueen

2

1. 当代码存在异常可能性或者问题中间可能出现异常时,需要在代码中包含异常检查。

2. 只有在必要的情况下使用try-catch块。每个try-catch块的使用都会增加额外的条件检查,这肯定会降低代码的优化程度。

3. 我认为_try_except是一个有效的变量名....


3
FYI,“__try”和“__except”是用于微软的结构化异常处理(SEH)。http://msdn.microsoft.com/en-us/library/s58ftw19(v=vs.80).aspx - axw

0

基本区别是:

  1. 一个会为你处理错误。
  2. 另一个是你自己处理。

    • 例如,你有一个可能会导致 0除以错误 的表达式。使用 try catch 1. 将会在发生错误时帮助你。或者你需要在 2. 中使用 if a==0 then..

    • 如果你不使用 try catch 来捕获异常,我认为它并非更快,只是简单地绕过了,如果出现 error,它将被 抛出 到外部处理程序。

自行处理意味着问题不会进一步扩大,在许多情况下具有速度优势,但并非总是如此。

建议:在简单且逻辑清晰的情况下,仅自行处理。


-2
许多 C/C++ 纯粹主义者完全不赞成使用异常。主要批评意见如下:
  1. 它很慢 - 当然,它并不真正“慢”。但是,与纯 C/C++ 相比,有相当多的开销。
  2. 它会引入错误 - 如果你没有正确地处理异常,你可能会错过抛出异常的函数中的清理代码。

相反,每次调用函数时请检查返回值/错误代码。


16
胡说八道。首先,我们谈论的是C++ - 没有“C/C ++”,当然仅使用C的程序员会对异常感到不舒服。“纯C/C ++”不存在,异常是“纯C ++”的一部分 - 它们在标准中。您可以编写错误的异常处理代码,也可以编写错误的错误返回代码或错误状态代码 - 将异常表现为普遍更容易出错是误导性的。抱歉,但这是我在S.O上看到的最糟糕的建议之一。 - Tony Delroy
1
如果你想打分的话,可以给我打分,但是作为一个有C语言背景的人,我可以告诉你,我和很多其他人都不使用异常。 - speedplane
4
  1. 只有在确实抛出异常时才会变慢。而异常被称为“异常”,是因为它们只在非常规情况下才会被抛出。每秒抛出超过9000个异常的程序是错误的做法。
  2. 如果你小心地避免使用纯new-delete进行内存管理,清理工作将会自动完成。C语言风格的内存管理比异常处理更容易出错。
  3. 检查返回值会增加很多额外的代码行,因此使代码阅读性和可维护性降低。
- Septagram
3
  1. 根据编译器的不同可能不是真的。所有异常都会增加字节码,导致可执行文件变大并且减慢运行速度。
  2. 纯的 new-delete 经常被使用,不是所有东西都可以成为堆栈对象。
  3. 检查返回值可能需要更多的代码行数,但这样做可能更易于维护。你可能不同意,但普遍认为 C++ 异常在许多情况下是不好的。以嵌入式 C++ 为例。
- speedplane
11个踩和8个赞。我认为很明显,虽然这种观点并不代表普遍共识,但有很多开发人员对于我所描述的原因对异常持谨慎态度。 - speedplane
@speedplane,你回到了一个十年前的答案,现在已经过去两年了,所以请原谅这个令人讨厌的“尸体复活”回复,但我认为这个答案的主要问题不是人们喜欢异常。我个人更喜欢尽可能避免使用它们,而现代编程语言大多都在向代数类型的内部错误处理转移。更有可能的是,人们只是不同意这个“纯洁性”的定义和对“C风格”的C++的推广。就像我上面说的,我不喜欢异常,但这种风格恰恰与我所谓的“纯C++”相反。 - Jesse Wyatt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接