C++中的`new`运算符在现实生活中是否可能抛出异常?

60

2
好的,你不应该在throw中使用new...正如其他人所提到的,除非你有处理它的策略,否则你不应该捕获异常。 - Potatoswatter
@OP:我已经更新了我的帖子,描述了一些情况,在这些情况下检查内存不足问题是明智的。 - C. K. Young
1
“recover”是什么意思?向用户提供连贯的错误信息有优点。然而,内存不足错误通常源于程序漏洞或大型数据集,无论哪种情况,添加更多内存都可能不足(在漏洞情况下,可能添加任何数量的内存都无济于事)。 - David Thornley
2
@osgx: 当然,我应该说一下,提供一条连贯的错误信息而不是崩溃,无论是向用户还是日志记录,都有其优点。 - David Thornley
我很惊讶没有回答提到内存池/区域。这是一个情况,实际上使用 a.allocate() 抛出 std::bad_alloc 是相当常见的。虽然它不是 new 运算符,但仍然...... - Mooing Duck
显示剩余3条评论
18个回答

47

是的,new操作符可能会抛出异常,如果内存不足或者尝试分配过大的内存块都可能导致这种情况。

你可以捕获std::bad_alloc异常并根据需要进行处理。有时候这样做是有意义的,但大多数情况下(也就是说,通常)并不需要处理它。例如,如果你尝试分配一个巨大的缓冲区,但实际上只需要较小的空间,那么你可以尝试逐步分配更小的块来解决问题。


8
当你写C语言时,你是否会检查malloc的返回值是否为NULL?如果不是,我怀疑我无法说服你注意从new中抛出的异常。 - ojrac
11
在任何可以合理恢复的地方,您应该捕获std::bad_alloc异常。在大多数情况下,您无法做太多事情,因此最好的选择可能是在main函数中捕获它,并且至少要向用户提供友好的错误消息或记录失败(包括Herb Sutter在内的许多专家都同意这一点:http://www.gotw.ca/publications/mill16.htm)。 - James McNellis
2
@osgx:在GNU程序中,惯例是将(malloc成功或死亡)函数称为xmalloc - C. K. Young
7
哎呀!你没有检查fclose()函数的返回值吗?! - NTDLS
4
我曾经从事嵌入式系统开发,可以确定如果您不检查new/malloc操作的成功与否,某个用户会找到一种方法来填充设备的内存。如果代码缺乏适当的检查,则会导致应用程序崩溃。不检查函数的返回值是一个非常不好的做法。 - karlphillip
显示剩余10条评论

27
新的操作符和new[] 操作符应该会抛出std::bad_alloc异常,但情况并非总是如此,因为这种行为有时可以被覆盖。
可以使用std::set_new_handler,这时可能会发生与抛出std::bad_alloc异常完全不同的事情。尽管标准要求用户要么提供可用内存、中止程序或抛出std::bad_alloc异常,但当然也可能不是这种情况。
免责声明:我不建议这样做。

7
这就是我所说的糟糕做法。 - Nathan Osman
7
std::set_new_handler<new> 中是标准的 C++,§18.4.2.2-3。如果您有某种垃圾回收机制,或者想记录错误,使用它是完全合理的。通过 throw bad_alloc 退出 new_handler 不是一个坏主意。 - Potatoswatter
3
标准要求用户的新处理程序必须提供内存,中止或抛出bad_alloc异常。 - Potatoswatter
1
cppreference中得知:"在分配失败的情况下,标准库实现会调用由std::get_new_handler返回的函数指针,并重复分配尝试,直到新处理程序不返回或成为空指针,在此时它会抛出std::bad_alloc异常。" 我正在做的事情基本上就是这里 - bit2shift

21

如果您正在运行一个没有虚拟内存的典型嵌入式Linux处理器上,如果您分配了太多内存,那么在新建失败之前,操作系统很可能会终止您的进程。

如果您的程序在物理内存少于虚拟内存最大值(标准Windows为2 GB)的机器上运行,则会发现一旦您分配的内存量大约等于可用的物理内存,进一步的分配将成功,但会导致分页到磁盘。这会拖慢您的程序,您实际上可能无法达到耗尽虚拟内存的点。因此,您可能不会抛出异常。

如果您有更多的物理内存而不是虚拟内存,并且您只需保持分配内存,那么当您耗尽虚拟内存以至于无法分配所请求的块大小时,您将收到一个异常。

如果您有一个长时间运行的程序,包括小块和寿命各异的各种不同块大小的分配和释放,那么虚拟内存可能变得碎片化,以至于新建将无法找到足够大的块来满足请求。然后新建将抛出异常。如果您偶尔泄漏随机位置的小块,则最终会将内存片段化到足以使任意小的块分配失败,并抛出异常。

如果您有一个程序错误,意外地向new[]传递了一个巨大的数组大小,则new将失败并抛出异常。例如,如果数组大小实际上是某种随机字节模式,可能源于未初始化的内存或损坏的通信流。

所有以上内容都是针对默认全局new的。但是,您可以替换全局new并提供类特定的new。它们也可以抛出异常,该情况的含义取决于您如何编程。通常,new包括一个循环,尝试获取请求的所有可能途径。当所有这些耗尽时,它会抛出异常。然后您要做什么取决于您。

您可以捕获new操作符的异常,并利用此机会记录异常发生时程序状态。您可以“dump core”(将内存转储到磁盘)。如果在程序启动时分配了循环仪表缓冲区,则可以在终止程序之前将其转储到磁盘。相比不处理异常,程序终止可以是温和的,这是一个优点。

我个人没有见过异常后可以获得额外内存的情况。然而,有一种可能性:假设您有一个内存分配器,它非常高效,但不能很好地回收已释放空间。例如,它可能容易出现空闲空间碎片化,即相邻但未合并的空闲块。您可以使用new操作符引发的异常,在new_handler中捕获,然后运行一种自由空间压缩过程,然后重试。

严肃的程序应将内存视为潜在的稀缺资源,尽可能控制其分配,监视其可用性,并在出现问题时采取适当措施。例如,您可以认为在任何真正的程序中,传递给内存分配器的大小参数有一个相当小的上限,而任何大于此上限的请求都应导致某种错误处理,无论是否可以满足请求。您可以主张应监视长期运行程序的内存增长率,并且如果可以合理预测程序将在不久的将来耗尽可用内存,则应开始有序重启进程。


简单说,(抱歉,不得不说这个笑话。:-P) - C. K. Young
1
+1 是为了解释,在现实生活中,new 操作符只有在程序员出错时才会抛出异常(操作系统通常会提供内存,除非你真的搞砸了)。 - Chromozon
是的,这个回答很长,但也是一个有用的回答,描述了几种真实情况下的行为,并且至少提供了一个信息,即在捕获std::bad_alloc时,必须先分配内存来优雅地处理它,否则也会失败! - A. Richard

10
在Unix系统中,通常使用内存限制(使用ulimit)运行长时间运行的进程,以便不会消耗所有系统的内存。如果您的程序达到了该限制,您将收到std::bad_alloc
更新:在垃圾收集系统中,程序从内存不足条件恢复的最典型情况是进行垃圾收集并继续执行。不过,这种按需GC真的只适用于最后的努力;通常,好的程序会定期进行GC以减少收集器的压力。
对于非垃圾收集程序来说,从内存不足问题中恢复比较不寻常,但对于面向互联网的服务器,一种恢复的方法是简单地拒绝导致内存耗尽的请求并返回一个“临时”错误。(“先到先得”策略)。

6
同时也包括图形用户界面应用程序。如果用户操作导致内存耗尽,则要放弃当前操作,但不要关闭整个应用程序。 - Martin York

9

osgx说:

有没有现实世界的应用程序可以检查大量新闻并在没有内存时进行恢复?

我之前在我的回答中回答过这个问题,这个问题已经被引用如下:

这种情况非常难处理。您可能希望向应用程序用户返回有意义的错误,但如果是由于内存不足引起的问题,您甚至可能无法承担分配错误消息所需的内存。这实际上是一个进退两难的情况。
有一种防御性编程技术(有时称为内存降落伞或备用金),在应用程序启动时分配一块内存。当您处理bad_alloc异常时,释放此内存,并使用可用内存优雅地关闭应用程序,包括向用户显示有意义的错误。这比崩溃要好得多 :)

7
这取决于编译器/运行时以及您正在使用的operator new(例如,某些版本的Visual Studio默认情况下不会抛出异常,而是返回一个类似于mallocNULL指针。)
您始终可以catch std::bad_alloc异常,或者明确使用nothrow new来返回NULL而不是抛出异常。(还请参阅过去的StackOverflow帖子有关此主题的讨论。)
请注意,与malloc类似,operator new在内存耗尽、地址空间耗尽(例如,在32位进程中为2-3GB,具体取决于操作系统)、配额耗尽(ulimit已经提到)或连续地址空间耗尽(例如,堆被分片)时会失败。

当它失败时,我可以做什么? - osgx
1
@osgx: 这个“bug”存在于Visual C++ 6 (VS98)和Visual C++ 2003上(但是你可以设置编译器选项让新行为像标准要求的那样)。它不是一个bug,而是出于向后兼容性而存在的不符合标准的行为。 - paercebal
@Vlad 如果您在编译代码时没有启用异常支持,我想它仍会以这种方式运行。 - Alex Jasmin

7

你不需要在每个new中处理异常 :) 异常可以传播。设计代码时,应该在每个“模块”中的某些点处理错误。


4

是的,new 可以抛出 std::bad_alloc 异常(std::exception 的子类),你可以捕获它。

如果你绝对想避免这个异常,并且准备测试 new 的结果是否为 null 指针,你可以添加一个 nothrow 参数:

T* p = new (nothrow) T(...);
if (p == 0)
{
    // Do something about the bad allocation!
}
else
{
    // Here you may use p.
}

3
我看到很多代码错误地假设没有参数的 new 在失败时会返回 NULL。 - Nathan Osman
那我每次使用 new 操作符时都必须检查这些 NULL 吗? - osgx
2
@osgx:只有在使用nothrow选项时才会如此。您有没有仔细阅读squelart的回答? - Billy ONeal
是的。当使用“nothrow”时,我需要检查“NULL”,或者能够在使用new的任何地方捕获“bad_alloc”吗?在大型程序中有成千上万这样的地方,这可能非常困难。 - osgx
然后只需在main()的末尾(如果您是多线程,则在每个线程方法的末尾)保留一个catch,并在退出之前显示一个大型错误消息“内存不足”。 :) - vladr
2
nothrow new 的合法用途很少。我能想到的两个是在处理遗留代码(假定 new 在失败时返回 null)或禁止使用异常的情况下(例如在嵌入式系统中)。 - James McNellis

4

是的,如果没有更多可用内存,new 将抛出异常,但这并不意味着您应该在每个 new 前加上 try ... catch 代码块。只有在程序可以真正处理异常情况时才要捕获异常。

如果程序无法处理异常情况(通常情况下,当内存耗尽时),那么捕获异常就没有意义了。如果唯一可以合理做的事情是中止程序,那么您也可以让异常沿袭到最高级别,在那里它将终止程序。


3
请注意,在Windows系统中,非常大的new/mallocs将直接从虚拟内存中分配。实际上,在你看到异常之前,你的机器就会崩溃。
char *pCrashMyMachine = new char[TWO_GIGABYTES];

敢试就来试试吧!


我该如何指定50TB?应用程序能处理这种情况吗?哪些版本的Windows会崩溃? - osgx
1
你让我启动编译器了!我的错误——50TB以上是行不通的。该值限制为2^31,约为2GB。因此,请在剩余磁盘空间小于2GB的机器上尝试实验。我最初在Windows XP上运行了这个实验。不知道其他版本的操作系统和MSVC运行时,而且这是一个非常烦人的实验。 - Erik Hermansen
@ErikHermansen 磁盘空间?首先,您需要释放更少的内存 - Michel de Ruiter

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