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


82

我正在阅读有关异常处理的信息。我获得了有关什么是异常处理的一些信息,但是我有几个问题:

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


您将需要询问比“何时引发异常”更具体的问题。当发生异常情况时,您将引发异常。
Falmarri 2010年

我写过有关PHP的文章,但我认为几乎所有内容都适用于C ++。查看博客文章
ircmaxell,

Answers:


95

这是关于异常的非常全面的指南,我认为这是必读的:

异常和错误处理-C ++ FAQC ++ FAQ Lite

作为一般经验法则,当程序可以识别外部妨碍执行的问题。如果您从服务器接收到数据并且该数据无效,则引发异常。磁盘空间不足?引发异常。宇宙射线会阻止您查询数据库?引发异常。但是,如果您从自己的程序中获取了一些无效数据,请不要抛出异常。如果您的问题来自您自己的错误代码,则最好使用ASSERT进行防范。需要异常处理来识别程序无法处理的问题并告诉用户有关的问题,因为用户可以处理它们。但是程序中的错误不是用户可以处理的,因此程序崩溃将告诉我们“ answer_to_life_and_universe_and_everything的值不是42!这永远不会发生!!! 11”异常。

捕获一个异常,您可以在其中执行一些有用的操作,例如显示一个消息框。我更喜欢在某个以某种方式处理用户输入的函数中捕获异常。例如,用户按下按钮“消除所有饥饿感”,并且在annihilateAllHunamsClicked()函数内部,有一个try ... catch块说“我不能”。尽管hunamkind的an灭是一项复杂的操作,需要调用数十个函数,但只有一个try ... catch,因为对于用户而言,这是一个原子操作-单击一次按钮。每个功能中的异常检查都是多余且丑陋的。

另外,我不建议您足够熟悉RAII-也就是说,要确保初始化的所有数据都被自动销毁。这可以通过在堆栈上尽可能多地初始化来实现,并且当您需要在堆上初始化某些内容时,请使用某种智能指针。引发异常时,堆栈上所有初始化的内容都会自动销毁。如果使用C风格的哑指针,则在引发异常时会冒内存泄漏的风险,因为没有人可以清除异常(当然,您可以将C风格的指针用作类的成员,但请确保它们是在析构函数中得到照顾)。


感谢您的宝贵意见。>“需要异常处理来识别程序无法处理的问题,并告诉用户有关的问题,因为用户可以>处理它们。”除了引发异常,我还可以返回错误值,并且可以在调用函数中检查错误并显示一条消息,以帮助用户进行处理。
Umesha MS 2010年

再次查看FAQ,特别是本节中的第三和第四代码段:parashift.com/c++-faq-lite/ exceptions.html #faq-17.2异常很棒,因为它们使您的错误从任何深度浮现。调用堆栈...这就像错误代码是水深圈,例外是潜艇:)
Septagram 2010年

2
@Septagram“如果您的问题来自您自己的错误代码,最好使用ASSERT来防御它。”:断言不会让您继续执行不依赖于错误分支的任务(只是因为您的程序具有一个功能中的错误并不意味着它不能做其他有用的事情)。它不会让您使用自定义记录器。它将使用RAII绕过您从中受益的析构函数(是否要在程序终止之前刷新文件缓冲区?最好不要再跳过那些析构函数fclose!)。它不会给您完整的堆栈跟踪信息。
daemonspring '17

关于RAII和“傻瓜指针”的话要说。仅仅因为您使用“哑指针”并不意味着您没有遵循RAII。请记住,只有在分配系统资源时才需要确保将其释放。指针只是堆栈上保存内存地址的变量。使用“哑指针”不需要对象分配和初始化。“哑指针”可以简单地保存分配给其他位置的对象的地址,并且使用起来非常好。(在下
一条

您可能具有一个Renderer对象,该对象将Display对象的地址存储在一个指针中,以便将内容绘制到Display上,但是您不能使用智能指针,因为当您破坏Renderer对象时,智能指针会破坏Display对象,您不想发生这种情况。在这种情况下,“哑指针”或引用是唯一可行的方法。
SeanRamey

12

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

首先,在某些函数中,计算前提条件的成本如此之高,最好只进行计算,如果发现不满足前提条件,则异常终止。例如,您不能反转奇异矩阵,但是要计算奇异矩阵,您需要计算非常昂贵的行列式:无论如何都可能必须在函数内部完成,因此只需“尝试一下”即可反转矩阵并报告如果无法通过引发异常则返回错误。作为否定的前提条件使用,这基本上是一个例外

在其他情况下,您的代码已经很复杂,并且很难将错误信息传递给调用链。部分原因是C和C ++的数据结构模型已损坏:还有其他更好的方法,但是C ++不支持它们(例如在Haskell中使用monad)。基本上,我不会理会这种用法,所以我会抛出一个例外:它不是正确的方法,但是很实用。

然后是例外的主要用途:在外部先决条件或不变量(例如内存或磁盘空间等足够的资源)不可用时进行报告。在这种情况下,您通常将终止程序或其主要子节,并且该异常是传输有关问题的信息的好方法。C ++异常旨在报告导致程序无法继续执行的错误

众所周知,包括C ++在内的大多数现代语言中使用的异常处理模型已破坏。它太强大了。与完全开放的“抛出任何东西”和“也许可能不抓到它”模型相比,理论家现在已经开发出了更好的模型。另外,使用类型信息对异常进行分类不是一个好主意。

因此,您可以做的最好的事情是:在出现实际错误时,并且在没有其他方法可以处理异常捕获尽可能接近抛出点的异常时,谨慎地抛出异常


15
您能否为“已知异常处理已被打破”和“理论家现在开发了更好的模型”添加一些链接/引用?
朱拉·布拉霍

1
捕获异常时通常需要知道的一个关键问题是,引发异常的代码是否对系统状态有任何影响,但是大多数异常没有提供有用的数据。有任何理论家的模型可以解决这个问题吗?
超级猫

@JurajBlaho我试图找到您正在谈论的链接/参考,但没有成功。您可以编辑他的答案以将其添加到最后吗?
ForceMagic 2012年

我同意示例#1和#3。但是,我确实认为,如果代码太复杂,那么您在考虑引发异常时就可能需要进行一些重构。声明也是检查代码是否正常运行的好方法(甚至更好),并且实际上比异常花费少。对于这门课程感兴趣的人,我推荐本书中的2章(断言编程和何时使用异常):The Pragmatic Programmer。
ForceMagic 2012年

1
无需链接。用脑子。您可以抛出未捕获的异常。那很糟。比goto更糟糕,因为goto至少总会出现在某个地方。静态类型不能满足异常规范:没有健全的系统可以在其中使用多态函数的异常规范,因为它依赖于类型变量的特定实例。请注意,这些已从新的C ++标准中删除。新模型称为“定界延续”:en.wikipedia.org/wiki/Delimited_continuation
Yttrill 2012年

4

如果您的问题来自您自己的错误代码,则最好使用ASSERT进行防范。需要异常处理来识别程序无法处理的问题,并告知用户有关的问题,因为用户可以处理它们。但是程序中的错误是用户无法处理的,因此程序崩溃并不能说明什么

我不同意接受的答案的这一方面。断言并不比抛出异常更好。如果异常仅适用于运行时错误(或“外部问题”),那么这有什么std::logic_error用?

从定义上讲,逻辑错误几乎是阻止程序继续的那种情况。如果程序是逻辑结构,并且条件发生在该逻辑的范围之外,那么该程序如何继续?尽可能收集您的输入,并抛出异常!

这就像没有现有技术一样。 std::vector,仅举一个,抛出一个逻辑错误异常,即std::out_of_range。如果您使用标准库并且没有顶级处理程序来捕获标准异常-如果仅调用what()并退出(3)-那么您的程序将遭受突然的静默,终止。

断言宏的防护性要弱得多。无法恢复。除非,也就是说,您没有运行调试版本,否则将无法执行。断言宏属于计算比现在慢6个数量级的时代。如果您要麻烦测试逻辑错误,但是在生产中不使用该测试,则最好对代码充满信心!

标准库提供逻辑错误异常,并采用它们。它们之所以存在是有原因的:因为发生逻辑错误,并且是异常情况。当异常可以更好地处理工作时,仅因为C功能断言就没有理由依赖这种原始的(可能是无用的)机制。


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
仅供参考,__ try和__except用于Microsoft的结构化异常处理(SEH)。msdn.microsoft.com/zh-CN/library/s58ftw19(v=vs.80).aspx
axw 2010年

0

基本区别是:

  1. 一个为您处理错误。
  2. 一个是你自己做。

    • 例如,您有一个表达式make 0 divide error。使用try catch1.将在发生错误时为您提供帮助。或者你需要一个if a==0 then..2.

    • 如果您不尝试捕获异常,我认为error它不会更快,只是绕过它,如果发生,它将threw传给外部处理程序。

交代自己意味着问题不会进一步发展,因此在许多情况下会在速度上有所优势,但并非总是如此。

建议:在逻辑上简单且逻辑上的情况下,请自行处理。


-3

许多C / C ++纯粹主义者完全阻止异常。主要批评是:

  1. 它很慢-当然不是真的“慢”。但是,与纯c / c ++相比,有很多开销。
  2. 它引入了错误-如果您不能正确处理异常,则可能会错过引发异常的函数中的清除代码。

而是在每次调用函数时检查返回值/错误代码。


15
废话。首先,我们谈论的是C ++-没有“ C / C ++”,当然,只有C语言的程序员会对异常感到不舒服。“纯C / C ++”不存在,并且例外是“纯C ++”的一部分-它们在标准中。您可以编写越野车的异常处理代码,也可以编写越野车的错误返回代码或越野车的错误状态代码-将异常特征化为一般更容易出错是一种误导。抱歉,但这是我在SO上看到的一些最糟糕的建议
Tony Delroy 2010年

1
如果愿意,可以将我标记为难,但是我来自C的背景,我可以告诉你我自己和许多其他人完全不使用异常。
speedplane

4
1.只有在实际引发异常时,它才很慢。异常称为异常,因为它们仅在特殊情况下抛出。程序每秒抛出9000个异常,这是错误的。2.如果您注意不要对纯new-delete进行内存管理,则将为您完成清理。C风格的内存管理比异常更容易出错。3.检查返回值会增加很多额外的代码行,从而使它的可读性和可维护性降低。
Septagram 2010年

3
1)取决于编译器可能不正确。而且所有异常都添加了大量字节代码,从而增加了可执行文件的大小并减慢了运行速度。2)始终使用纯new-delete,并非所有内容都可以作为堆栈对象。3)检查返回值可能需要更多的代码行,但是可以说它更易于维护。您可能不同意,但是人们普遍认为,C ++异常在许多情况下是不好的。以嵌入式C ++为例。
speedplane

11票和8票。我认为很明显,尽管这种意见并不能反映普遍共识,但出于我所描述的原因,仍有一大批开发人员对例外情况保持警惕。
speedplane
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.