【问题标题】:How to deal with bad_alloc in C++?如何处理 C++ 中的 bad_alloc?
【发布时间】:2012-03-16 10:28:41
【问题描述】:

有一个名为foo的方法有时会返回以下错误:

terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
Abort

有没有办法可以使用try-catch 块来阻止此错误终止我的程序(我要做的就是返回-1)?

如果有,它的语法是什么?

我还能如何在 C++ 中处理 bad_alloc

【问题讨论】:

  • 您是否尝试编写try ... catch 语句?哪一个??
  • 我从未见过 bad_alloc 异常。我想我必须分配不正确数量的资源。你在做什么导致这种情况发生?
  • 你能更具体更准确吗? “存在”、“有时”、“我想做的所有事情”是什么意思...捕获异常在技术上很容易,但在大多数情况下,如果不了解其语义就这样做是个坏主意。
  • 另外标题和你的问题描述不一致。

标签: c++ memory-management bad-alloc


【解决方案1】:

一般情况下您不能,并且不应该尝试来响应此错误。 bad_alloc 表示无法分配资源因为没有足够的内存可用。在大多数情况下,您的程序无法应对这种情况,而尽快终止是唯一有意义的行为。

更糟糕的是,现代操作系统经常过度分配:在这样的系统上,mallocnew 可以返回一个有效的指针,即使没有足够的可用内存 - std::bad_alloc 永远不会被抛出,或者在至少不是内存耗尽的可靠迹象。相反,尝试访问分配的内存将导致无法捕获的分段错误(您可以处理分段错误信号,但之后您无法恢复程序)。

在捕获std::bad_alloc 时,您唯一可以做的就是记录错误,并尝试通过释放未完成的资源来确保安全的程序终止(但这是在错误发生后的正常堆栈展开过程中自动完成的如果程序正确使用 RAII,则抛出)。

在某些情况下,程序可能会尝试释放一些内存并重试,或者使用辅助内存(= 磁盘)而不是 RAM,但这些机会仅存在于具有严格条件的特定场景中:

  1. The application must ensure that it runs on a system that does not overcommit memory,即它表示分配失败而不是稍后。
  2. 应用程序必须能够立即释放内存,同时不会有任何进一步的意外分配。

应用程序控制第 1 点的情况极为罕见——用户空间应用程序从不这样做,这是一个系统范围的设置,需要 root 权限才能更改。1

好的,假设您已经确定了第 1 点。例如,您现在可以对某些数据使用 LRU cache(可能是一些可以按需重新生成或重新加载的特别大的业务对象)。接下来,您需要将可能失败的实际逻辑放入支持重试的函数中——换句话说,如果它被中止,您可以重新启动它:

lru_cache<widget> widget_cache;

double perform_operation(int widget_id) {
    std::optional<widget> maybe_widget = widget_cache.find_by_id(widget_id);
    if (not maybe_widget) {
        maybe_widget = widget_cache.store(widget_id, load_widget_from_disk(widget_id));
    }
    return maybe_widget->frobnicate();
}

…

for (int num_attempts = 0; num_attempts < MAX_NUM_ATTEMPTS; ++num_attempts) {
    try {
        return perform_operation(widget_id);
    } catch (std::bad_alloc const&) {
        if (widget_cache.empty()) throw; // memory error elsewhere.
        widget_cache.remove_oldest();
    }
}

// Handle too many failed attempts here.

但即使在这里,使用std::set_new_handler 而不是处理std::bad_alloc 提供了相同的好处并且会更简单。


1 如果您正在创建一个确实控制点 1 的应用程序,并且您正在阅读此答案,请给我发电子邮件,我真的很好奇关于你的情况。

【讨论】:

  • @KonradRudolph,说“不能也不应该尝试”来回应这个错误可能有点过分了。如果该程序位于敏感主机(核电站、机器人、火星探测器)中,让自己被异常杀死是不可接受的怎么办?如果您可以真正处理它怎么办?一个更简单的例子是加载对象的游戏。如果没有足够的内存,它可以尝试使用质量较低的网格重新加载,或者减少粒子的数量,或者为丢失的任何对象显示立方体等。
  • @DavidHammen,这是真的。实际上它可能无论如何都会用 C 编写,因为 C++ 的许多特性对于系统编程来说太高级了,并且比 C 更难以控制。我的意思是说:“通常你对 bad_alloc 无能为力,所以你可能会好吧,被杀”是可以的,但是说“你不应该检查 bad_alloc”是太多了。您无法想象每个用例。对于某些人来说,也许检查 bad_alloc 实际上很有用。
  • @SHahbaz,总是可以走 Matlab 过去的路线。假设用户无意中请求了几乎所有的虚拟内存。知道机器尺寸后,Matlab 认为“我可以做到!但这些事情必须精细地完成!”它并没有一下子把所有的记忆都抹去。相反,它是一块一块的。一旦失败,它就会进入一个繁忙的循环并一次又一次地重试。当其他一些进程退出时,Matlab 会尽快吞噬该内存。最终,除了Matlab野兽之外什么都没有了,除了重新启动之外没有其他方法可以杀死它。捕获分配失败不是一个好主意。
  • @KonradRudolph:“无论如何,您不会从中获得有意义的诊断,因为大多数现代系统从不会失败 malloc,它们只是将不存在的内存标记为保留。” - 只是一个想法,但也许应该将此评论编辑为答案本身,因为它是解释为什么尝试处理 bad_alloc 可能无用或不可靠的关键见解......
  • “大多数现代系统永远不会失败 malloc” - 这根本不是真的。 Malloc 和 new 在 Windows 上肯定会失败,Windows 仍然是 2015 年最广泛使用的操作系统。不幸的是,支持 32 位应用程序对于大多数供应商来说仍然很重要,虚拟地址空间不足或内存碎片是常见问题。 (顺便说一句,Windows 上的 64 位应用程序也可能会失败。)
【解决方案2】:

c++ 中new 的 C++ 标准规定的行为是什么?

通常的想法是,如果new 运算符无法分配所请求大小的动态内存,那么它应该抛出std::bad_alloc 类型的异常。
但是,甚至在引发 bad_alloc 异常之前,还会发生更多事情:

C++03 第 3.7.4.1.3 节:

分配存储失败的分配函数可以调用当前安装的 new_handler(18.4.2.2),如果有的话。 [注意:程序提供的分配函数可以使用 set_new_handler 函数 (18.4.2.3) 获取当前安装的 new_handler 的地址。] 如果使用空异常规范 (15.4) 声明的分配函数 throw() 失败分配存储,它应该返回一个空指针。任何其他分配存储失败的分配函数只能通过抛出类 std::bad_alloc (18.4.2.1) 或从 std::bad_alloc 派生的类的异常来指示失败。

考虑以下代码示例:

#include <iostream>
#include <cstdlib>

// function to call if operator new can't allocate enough memory or error arises
void outOfMemHandler()
{
    std::cerr << "Unable to satisfy request for memory\n";

    std::abort();
}

int main()
{
    //set the new_handler
    std::set_new_handler(outOfMemHandler);

    //Request huge memory size, that will cause ::operator new to fail
    int *pBigDataArray = new int[100000000L];

    return 0;
}

在上面的例子中,operator new(很可能)将无法为 100,000,000 个整数分配空间,函数 outOfMemHandler() 将被调用,程序将在 issuing an error message. 之后中止

正如这里所见,new 运算符在无法满足内存请求时的默认行为是重复调用new-handler 函数,直到它找到足够的内存或没有更多新的处理程序。在上面的示例中,除非我们调用std::abort(),否则outOfMemHandler() 将是called repeatedly。因此,处理程序应该确保下一次分配成功,或者注册另一个处理程序,或者不注册处理程序,或者不返回(即终止程序)。如果没有新的handler,分配失败,操作符会抛出异常。

new_handlerset_new_handler 是什么?

new_handler 是指向函数指针的 typedef,该函数不接受和返回任何内容,set_new_handler 是接受并返回 new_handler 的函数。

类似:

typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();

set_new_handler 的参数是一个指向函数运算符new 的指针,如果它不能分配所请求的内存,则应该调用它。它的返回值是一个指向先前注册的处理函数的指针,如果没有先前的处理函数,则返回 null。

如何在 C++ 中处理内存不足的情况?

鉴于new的行为,一个设计良好的用户程序应该通过提供一个适当的new_handler来处理内存不足的情况,该new_handler执行以下操作之一:

提供更多可用内存:这可能允许 operator new 循环内的下一次内存分配尝试成功。实现这一点的一种方法是在程序启动时分配一大块内存,然后在第一次调用 new-handler 时释放它以供程序使用。

安装不同的 new-handler: 如果当前的 new-handler 不能提供更多可用内存,并且有另一个 new-handler 可以,那么当前的 new-handler 可以在其位置安装另一个新处理程序(通过调用set_new_handler)。下一次 operator new 调用 new-handler 函数时,它将获得最近安装的那个。

(这个主题的一个变体是让 new-handler 修改它自己的行为,所以下次调用它时,它会做一些不同的事情。实现这一点的一种方法是让 new-handler 修改静态的、命名空间-影响新处理程序行为的特定或全局数据。)

卸载新处理程序:这是通过将空指针传递给set_new_handler 来完成的。如果没有安装 new-handler,operator new 将在内存分配不成功时抛出异常((可转换为)std::bad_alloc)。

抛出异常可转换为std::bad_allocoperator new 不会捕获此类异常,但会传播到发起内存请求的站点。

不返回:通过调用abortexit

【讨论】:

  • 这并不能直接回答所问的确切 Q,(Konrad 的回答已经很好地回答了)但是这个答案提供了一个关于在 @987654351 之前在 c++ 中处理内存不足条件的不太为人所知的方法的见解@被抛出。
  • 这允许设置您自己的错误处理程序并在需要时显示堆栈回溯,从而足够精确地定位问题所在。
  • 这对我不起作用,如果交换被禁用,程序仍然不会捕获我的呼叫并冻结(arch linux)
【解决方案3】:

您可以像任何其他异常一样捕获它:

try {
  foo();
}
catch (const std::bad_alloc&) {
  return -1;
}

从这一点开始,你能做什么有用的事情取决于你,但它在技术上绝对是可行的。

【讨论】:

    【解决方案4】:

    我不建议这样做,因为bad_alloc 表示您内存不足。最好是放弃而不是试图恢复。但是,这是您要求的解决方案:

    try {
        foo();
    } catch ( const std::bad_alloc& e ) {
        return -1;
    }
    

    【讨论】:

    • @Wolf:这个问题明确表示“阻止这个错误终止我的程序(我想做的就是返回-1)” - 正如康拉德的回答中所讨论的那样,它可能有用也可能没有用,但这是所要求的(与您的答案 exit() 不同)。
    • @TonyD 是的,你是对的,他明确表示他不建议这样做。今天我不会对此投反对票。 我的技术问题现在我不能再改变这个过早的决定了,因为它发生得太早了。
    • SamMiller 你认为有可能改进你的答案,以便它可以与Flexo's 竞争...我真的很想(至少)取消投票;)
    • 我已经做了一个suggestion ...被接受了:)
    【解决方案5】:

    我可能会为此建议一个更简单(甚至更快)的解决方案。如果无法分配内存,new 运算符将返回 null。

    int fv() {
        T* p = new (std::nothrow) T[1000000];
        if (!p) return -1;
        do_something(p);
        delete p;
        return 0;
    }
    

    我希望这会有所帮助!

    【讨论】:

      【解决方案6】:

      让你的 foo 以可控的方式编程exit

      #include <stdlib.h>     /* exit, EXIT_FAILURE */
      
      try {
          foo();
      } catch (const std::bad_alloc&) {
          exit(EXIT_FAILURE);
      }
      

      然后编写一个调用实际程序的shell 程序。由于地址空间是分开的,所以你的 shell 程序的状态总是很明确的。

      【讨论】:

        【解决方案7】:

        当然,您可以捕获bad_alloc,但我认为更好的问题是如何首先阻止bad_alloc 的发生。

        通常,bad_alloc 表示内存分配出现问题 - 例如内存不足时。如果您的程序是 32 位的,那么当您尝试分配 >4 GB 时就会发生这种情况。当我将 C 字符串复制到 QString 时,这发生在我身上一次。 C 字符串不是以 '\0' 结尾的,这导致 strlen 函数返回数十亿的值。于是它试图分配几 GB 的 RAM,这导致了 bad_alloc

        我还看到bad_alloc 意外访问了构造函数的初始化列表中的未初始化变量。我有一个班级foo 和一个成员T bar。在构造函数中,我想用参数中的值初始化成员:

        foo::foo(T baz) // <-- mistyped: baz instead of bar
        : bar(bar)
        {
        }
        

        因为我打错了参数,构造函数用自己初始化了 bar(所以它读取了一个未初始化的值!)而不是参数。

        valgrind 对此类错误非常有帮助!

        【讨论】:

        • 您可以分配两个大小为 3 GB 的缓冲区。那会破坏你的地址空间。
        猜你喜欢
        • 2014-10-20
        • 2017-01-12
        • 1970-01-01
        • 2021-11-12
        • 1970-01-01
        • 1970-01-01
        • 2011-06-05
        • 2015-12-18
        相关资源
        最近更新 更多