当构造函数抛出未经处理的异常时会发生什么?

9
当构造函数中抛出未处理的异常时,会发生什么?对于Java和C++,会发生什么?是否会有内存泄漏?

对于Java,程序不会崩溃吗?你的IDE可能甚至都不会让你编译它。 - Michael
我认为在Java中你不必担心这个问题,如果在C++中可能会出现这种情况,那么请使用std::auto_ptr。 - Name
关于Java,您可以阅读Jon Skeet在其他SO问题中的答案:https://dev59.com/eXM_5IYBdhLWcg3wdy1g#1371559 - lpinto.eu
“未处理的异常被抛出”是什么意思?如何抛出已处理的异常? - Kerrek SB
@KerrekSB:相当确定这意味着“抛出异常而未处理”。 - Dietrich Epp
1
重要的是要意识到,在C++中,成员通常在构造函数抛出异常时被销毁。当然,在未捕获的异常之后,程序将结束,这使得这个问题相当学术化。 - MSalters
8个回答

7
您问道:
“当构造函数抛出未处理的异常时会发生什么?对于Java和C++,是否会出现内存泄漏?”
一个未处理的异常是指没有关联处理程序的异常。
在C++中,任何未处理的异常都会终止程序。这种情况下,堆栈是否被解开是未指定的,即成功构建的局部变量的析构函数可能会执行或不执行,这取决于编译器。异常抛出的位置(如在构造函数中)是无关紧要的。
“C++11 §15.3/9”: “如果找不到匹配的处理程序,则调用函数std::terminate();在调用std::terminate()之前是否解开堆栈是实现定义的。”
在Java中,未处理的异常同样会终止程序,或者至少终止当前线程(如果它不是主线程),但保证调用finally子句:
“Java SE 7语言规范§11.3”: 如果找不到能够处理异常的catch子句,则终止当前线程(遇到异常的线程)。终止之前,所有finally子句都将被执行[...]”
由于程序终止,因此实际上该程序本身不存在内存泄漏问题,因为实际上操作系统会清理进程后的资源。
然而,崩溃的程序可能会在磁盘上留下临时文件,并且可能会在服务器进程中泄漏其他资源,包括这些服务器进程中的内存泄漏。

1
@匿名的点踩者:请解释一下你的点踩,这样其他人就可以从你的见解中学习。 - Cheers and hth. - Alf
@Cheers-and-hth-alf,你的回答原版非常简短,可以概括为“我不知道,我不知道,我不知道”,这是我认为质量不高的回答(看起来像是在问题得到实际回答之前匆忙发帖占个位置的做法,这种做法正在困扰着这个论坛)。通过编辑和标准引用,它已经达到了我的要求。取消了踩的投票,它已经达到了目的。 - Mikael Persson
@MikaelPersson:感谢您解释了那个投票。最初我写道我不知道Java的final块是否被处理。之后我添加了标准和规范引用,解决了这个问题。所以在你的情况下,似乎至少有一个明确的声明使得你认为现在的内容与“dunno dunno dunno”相同。我认为这意味着在SO上,我需要优先考虑不将内容的感知引导到毫无价值的方向,而不是将技术问题的感知引导到过早的结论。 :-) - Cheers and hth. - Alf
1
调用std::terminate并不完全等同于终止程序。 - Arthur P. Golubev

6

对于Java语言: 控制流程会像在常规方法中抛出异常一样返回给调用者。不存在内存泄漏(未完成的实例将被丢弃并进行垃圾回收)。


2
在C++中,未处理的异常会一直持续到达到main()函数并关闭程序。未释放的内存将由操作系统处理。
不确定这是否回答了你的问题?
基本上,这就像从任何其他函数抛出异常一样。

在C++中,异常传播的意义上,并不能保证异常会“继续下去”。但是不清楚你是在谈论传播还是寻找处理程序。我认为因为不清楚而投反对票是不正确的,所以我只是留下这个评论,让你可以修正你的答案。 - Cheers and hth. - Alf

1
如果您在构造函数内创建依赖对象,则可能会发生内存泄漏。无论在哪种语言/环境中,如果这些依赖项被外部实体引用并且不清理它们,都可能导致泄漏。在JAVA和C#中,如果没有外部引用这些依赖项,则不会导致泄漏。垃圾回收器最终将清理它们。但是在C++中,如果没有外部引用这些依赖项,则肯定会导致泄漏。请参见Jon的答案以获取更多可能性:Java中的构造函数是否可以抛出异常?

0
值得注意的是:
1)Java区分“已检查”和“未检查”异常
2)大多数用户定义的异常应该是“已检查”的。这意味着,除非调用链中的每个模块都处理异常或明确标记它可以“抛出”异常,否则代码甚至不会编译。

大多数异常都必须是未经检查的! - lpinto.eu
Java文档中得出以下结论:底线指南是:如果可以合理地期望客户端从异常中恢复,将其作为已检查的异常。如果客户端无法从异常中恢复,则将其作为未经检查的异常。 - paulsm4

0

常见问题解答中好像没有涉及未处理异常的问题。然而,可能OP的意思是问“如果构造函数抛出异常会发生什么”,而不是“如果构造函数抛出未处理异常会发生什么”。如果是这样,如果OP的意思与所问的完全不同,那么你的回答可能会很有价值。 - Cheers and hth. - Alf

-1

是否出现内存泄漏取决于代码编写方式。如果编写“好”的代码,就不应该出现内存泄漏。但是完全有可能出现情况,使得一切变得非常糟糕。

如果构造函数在构造过程中分配了任何内容,则可能会出现问题。

解决方案通常是使用所谓的“两阶段构建”,因此构造函数本身非常简单且“不会出错”。一旦对象构造完成,您调用一个成员函数以以可能失败的方式填充对象,然后可以随意抛出异常,并确保在某个时候运行析构函数,所有事情都应该顺利进行。请注意“部分构造对象在析构函数中”的问题-如果指针为NULL或其他某些东西在析构函数的中途没有被构造,会发生什么。

[以上内容受“在返回主函数之前有处理程序,并且我们确实想做一些其他事情而不是终止整个程序”的影响]。


至少对于C++而言,两阶段构造不是答案,而是一个问题。这是一个非常著名的问题,我们正在涉及基础知识。在Bjarne Stroustrup的《C++程序设计语言》第三版中关于标准库中异常安全性的附录中有一些讨论(尽管不完整),并且可以从Bjarne的网站上以PDF格式获得该附录。 - Cheers and hth. - Alf
那么,在构造函数中抛出异常更好吗?当然,两种解决方案都不好,但如果你必须抛出异常,请不要在构造函数中这样做。 - Mats Petersson
2
从构造函数中抛出异常可以确保没有不完整的对象存在。C++的创建机制支持此操作,这也是所有优质代码处理构造失败(包括标准库)的方式。但对于C++来说,它与本问题无关,因为“在构造函数中”的限定词是无关紧要的。 :-) - Cheers and hth. - Alf
2
在C++中,双阶段构造是一个可怕的想法——几乎整个语言和标准库都是围绕在构造函数中获取资源并在析构函数中释放资源的思想设计的。双阶段构造意味着对象存在“已构造但无法使用”的值,这增加了必须考虑的状态量,并降低了类型安全性。最好遵循“使用显式资源管理类来管理资源,每个类只管理单个资源”的指导方针。 - Mankarse

-1

在C++或Java中,情况相似但又不同。

当异常被抛出时,它会向上回溯堆栈,寻找处理程序。在C++或Java中,可能永远找不到处理程序,因此一直回溯到起点并终止程序。在Java中,有一个叫做已检查异常的概念,它强制要求对异常进行某种处理(如果已检查)。在C++中,有一个叫做异常规范的概念,但它是不切实际的(设计不良),不应该使用,因此,在C++中将所有异常视为“未经检查”的。

无论异常最终终止程序还是在抛出异常的地方的上游某处被捕获,导致这种情况的展开过程才是重要的。如果最终终止程序,那么当操作系统回收内存时,就不会有内存泄漏。你需要担心的是:

  • 如果异常最终在上游某处被处理,则在展开期间可能会发生内存泄漏;以及
  • 其他可能泄漏的资源类型(例如,挂起的数据库操作、网络连接等),因为如果程序终止,操作系统将无法回收/撤消这些资源。
在C++中,随着堆栈展开的发生,保证每个完全构造的堆栈限定对象(包括正在构造的对象的数据成员或基类实例)将按照它们创建的确切相反顺序立即销毁(即确定性)。因此,只要所有资源直接绑定到对象的构造/销毁(也称为“RAII”),在展开过程中就不会有泄漏(内存或其他资源),因为每个成功获取的资源都将被释放(除非在展开期间释放资源失败,这是需要小心处理的事情)。
在Java中,“堆栈”展开以相同的方式发生,只是不会立即销毁对象,而是将它们标记为已丢弃(即将被垃圾回收),并最终在未来某个不确定的时间点被销毁。这保证了没有内存泄漏,只要垃圾回收器足够长时间存活以执行其工作,如果程序最终在未处理的异常上终止,我不确定是否有保障(但在那时已经无关紧要)。Java中的主要问题是其他资源。这些资源必须在finally块中释放。finally块在展开期间保证被执行,但是,当然,它们必须包含释放在相应的try块中分配的资源的代码。只要程序员做好了他的工作,资源就不会泄漏。
事实上,异常是从构造函数抛出的并没有什么区别,基本规则仍然是在抛出异常时不泄漏资源的基本规则:
  • 在C++中,将每一个单独的资源(包括内存和其他资源)与一个单独的对象绑定,在语言层面上保证没有泄漏。这就是资源获取即初始化(RAII)的惯用法。
  • 在Java中,确保将每一个单独的非内存资源获取放在自己的try块中,该块有自己的finally块来释放该单一资源。

在两种情况下,你必须清理地释放你的资源(无异常抛出)。


1
在C++或Java中,“它可能永远找不到一个并因此解开所有的方式回到起点并终止程序”是不正确的。对于未处理的异常,C++不保证堆栈展开。 - Cheers and hth. - Alf
1
确实,这就是为什么你应该在主函数上使用一个带有 try 块的函数,捕获所有异常。这将确保即使在本来不支持堆栈展开的系统上也能进行堆栈展开。 - MSalters

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