【问题标题】:Should you free an array if allocating that array threw an exception?如果分配该数组引发异常,您是否应该释放该数组?
【发布时间】:2019-03-20 15:12:21
【问题描述】:

我有一个其他人编写的可能不稳定的类,我必须创建该类的对象数组。我提到这个类是不稳定的,所以它可能偶尔会在默认构造函数中抛出异常。我无法访问源代码,只能访问已编译的二进制文件。

当我使用new 分配这些类型的对象的动态数组时,这些坏对象之一可能会引发异常。它抛出一个自定义异常,而不是std::bad_alloc。 无论如何,我需要让程序从异常中恢复并继续运行,尽管设置了一些错误标志等等。我认为我应该delete与数组关联的内存以防止内存泄漏。

我的理由是,如果类在数组中间某处构造元素时抛出异常,则该元素将无法正确构造,并且所有未来元素都将因异常而停止构造,但之前的元素自从抛出异常之前就已经正确构造了。我想知道,在catch (...) { } 中调用delete 是个好主意吗?我将如何解决这个内存泄漏?

Badclass* array = nullptr;

try {
  array = new Badclass[10];  // May throw exceptions!
} catch (...) {
  delete[] array;
  array = nullptr;
  // set error flags
}

这是我在记忆中形象化的方式。这是正确的吗?

 array         0    1    2    3   4    5 6 7 8 9
 ___          __________________________________
| ---------->| :) | :) | :) | :) | :( | | | | | |
|___|        |____|____|____|____|____|_|_|_|_|_|

【问题讨论】:

  • 如果new 抛出,array 从未被分配,所以delete 对你来说没有任何意义。因此,即使异常来自构造其中一个元素,该语言也保证内存被释放。
  • @BoBTFish 问题是,new 没有投掷,Badclass::Badclass() 是!
  • 尽管如此,new 是调用 Badclass::Badclass() 的东西,所以 new 永远不会完成
  • @Galaxy 没关系。如果请求的操作(分配和构造这一系列事物)抛出,那么与这两个活动相关的所有事情都会被退回。这包括分配和任何成功构造的对象(以及十个对象的数组请求满足五个,然后抛出,将按照构造的相反顺序为成功构造的五个触发析构函数,然后放弃内存本身并执行扔)。
  • 它基于请求操作的成功。无法满足请求。因此,任何可能发生的事情都被撤消并抛出异常。这包括破坏已经构建的元素。并不是说任何人都会这样做(您可能只使用std::vector 并完成它,或者有充分的理由不这样做)。

标签: c++ exception-handling


【解决方案1】:

出现异常无需调用delete:

array = new Badclass[10];  // May throw exceptions!

没有内存泄漏。

作为参考阅读new expression on cppreference

如果初始化因抛出异常而终止(例如,从 构造函数),如果 new-expression 分配了任何存储空间,它会调用 适当的释放函数:非数组类型的操作符删除, 数组类型的操作符 delete[]

所以它明确指出delete[] 是自动调用的,您不必调用它。

如果其中的部分对象是在抛出异常之前由new[] 构造的,那么所有已构造的对象都将在释放内存之前被破坏。这就像对象构造包含一个数组一样,在数组中构造某个对象时会抛出异常。

【讨论】:

    【解决方案2】:

    在以下代码行中:

    array = new Badclass[10];  
    

    new Badclass[10] 首先被评估。如果这引发异常,则执行不会到达分配。 array 保留其先前的值 nullptr。 对 nullptr 调用 delete 无效。

    来自 cmets 部分的问题:

    这种行为是否基于与堆栈展开相同的原理?

    标准中的“异常处理”部分帮助我们了解在分配时抛出异常时会发生什么。

    18 异常处理[除外]
    ...
    18.2 构造函数和析构函数 [except.ctor]

    1.当控制从抛出异常的点传递到处理程序时,析构函数由进程调用,在本子条款中指定,称为堆栈展开
    ...
    3.如果通过委托构造函数以外的对象的初始化或销毁因异常而终止,则为对象的每个直接子对象调用析构函数,对于完整对象,调用虚拟基类子对象,其初始化已完成且其destructor 尚未开始执行,除了在销毁的情况下,不会销毁类联合类的变体成员。子对象按照其构建完成的相反顺序销毁。这种销毁是在进入构造函数或析构函数的函数try块的处理程序之前排序的。

    【讨论】:

      【解决方案3】:

      回答最后一个问题:

      我将如何解决这个内存泄漏问题?

      没有内存泄漏。只有当BadClass 本身动态分配内容并且从未在其析构函数中释放它时,才会发生泄漏。由于我们没有注意到您的 BadClass 实施,而不是猜测,这取决于您。 new BadClass[N]; 本身泄漏内存的唯一方法是,如果它完成并且您稍后丢弃您手动管理的对它的唯一引用 (array)。

      一个动态分配的数组,在其中一个构造函数中抛出元素,将 (a) 以相反的顺序为已构造的元素退出析构函数,(b) 释放分配的内存,最后 (c) 执行实际的throw 到最近的 catch 处理程序(或没有的默认处理程序)。

      因为发生了抛出,所以对结果数组指针的赋值永远不会发生,因此不需要delete[]

      最好的例子:

      #include <iostream>
      
      struct A 
      {
          static int count;
          int n;
      
          A() : n(++count)
          {
              std::cout << "constructing " << n << '\n';
              if (count >= 5)
                  throw std::runtime_error("oops");
          }
      
          ~A()
          {
              std::cout << "destroying " << n << '\n';
          }
      };
      
      int A::count;
      
      int main()
      {
          A *ar = nullptr;
          try
          {
              ar = new A[10];
          }
          catch(std::exception const& ex)
          {
              std::cerr << ex.what() << '\n';
          }
      }
      

      输出

      constructing 1
      constructing 2
      constructing 3
      constructing 4
      constructing 5
      destroying 4
      destroying 3
      destroying 2
      destroying 1
      oops
      

      请注意,由于元素“5”的构造从未完成,因此不会触发其析构函数。但是, 成功构造的成员 被破坏(在上面的示例中没有演示,但如果你愿意的话,这是一个有趣的练习)。

      综上所述,无论如何都要使用智能指针。

      【讨论】:

      • @Galaxy 很高兴为您提供帮助。现在。停止在 C++ 中使用手动内存管理 =P 坚持使用 RAII 成语。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-01-08
      • 2016-11-09
      • 2020-09-23
      • 2010-10-06
      • 1970-01-01
      • 1970-01-01
      • 2012-02-10
      相关资源
      最近更新 更多