【问题标题】:new[] / delete[] and throwing constructors / destructors in C++new[] / delete[] 并在 C++ 中抛出构造函数 / 析构函数
【发布时间】:2017-12-22 06:04:05
【问题描述】:

在下面的代码中,如果某些数组元素的构造/销毁抛出了,会发生什么?

X* x = new X[10]; // (1)
delete[] x;       // (2)

我知道可以防止内存泄漏,但另外:

  1. Ad (1),之前构造的元素是否被破坏了?如果是的话,如果析构函数在这种情况下抛出会发生什么?

  2. Ad (2),未销毁的元素是否已销毁?如果是,如果析构函数再次抛出会发生什么?

【问题讨论】:

    标签: c++ arrays exception new-operator delete-operator


    【解决方案1】:
    1. 是的,如果x[5]的构造函数抛出,那么已经构造成功的5个数组元素x[0]..x[4]会被正确销毁。

      • 析构函数不应抛出。如果析构函数确实抛出,则会在前一个(构造函数)异常仍在处理时发生。由于不支持嵌套异常,因此会立即调用 std::terminate。这就是为什么析构函数不应该抛出。
    2. 这里有两个互斥的选项:

      1. 如果您达到标签(2),构造函数没有抛出。也就是说,如果x创建成功,那么十个元素全部构造成功。在这种情况下,是的,它们都被删除了。不,你的析构函数仍然不应该抛出。

      2. 如果构造函数在步骤 (1) 的中途抛出,则数组 x 从未真正存在过。该语言尝试为您创建它,但失败了,并引发了异常 - 所以您根本无法到达 (2)

    要理解的关键是x 要么存在 - 处于理智和可预测的状态 - 要么不存在。

    如果构造函数失败,该语言不会给您一些无法使用的半初始化的东西,因为无论如何您都无法对它做任何事情。 (你甚至不能安全地删除它,因为没有办法跟踪哪些元素是构造的,哪些只是随机垃圾)。

    将数组视为具有十个数据成员的对象可能会有所帮助。如果您正在构造此类的实例,并且基类或成员构造函数之一抛出,所有先前构造的基类和成员都会以完全相同的方式销毁,并且您的对象永远不会开始存在。

    【讨论】:

      【解决方案2】:

      我们可以用下面的代码进行测试:

      #include <iostream>
      
      //`Basic` was borrowed from some general-purpose code I use for testing various issues 
      //relating to object construction/assignment
      struct Basic {
          Basic() { 
              std::cout << "Default-Constructor" << std::endl; 
              static int val = 0;
              if(val++ == 5) throw std::runtime_error("Oops!");
          }
          Basic(Basic const&) { std::cout << "Copy-Constructor" << std::endl; }
          Basic(Basic &&) { std::cout << "Move-Constructor" << std::endl; }
          Basic & operator=(Basic const&) { std::cout << "Copy-Assignment" << std::endl; return *this; }
          Basic & operator=(Basic &&) { std::cout << "Move-Assignment" << std::endl; return *this; }
          ~Basic() noexcept { std::cout << "Destructor" << std::endl; }
      };
      
      int main() {
          Basic * ptrs = new Basic[10];
          delete[] ptrs;
          return 0;
      }
      

      此代码在崩溃之前产生以下输出:

      Default-Constructor
      Default-Constructor
      Default-Constructor
      Default-Constructor
      Default-Constructor
      Default-Constructor
      [std::runtime_error thrown and uncaught here]
      

      请注意,在任何时候都没有调用析构函数。这不一定是关键的事情,因为未捕获的异常无论如何都会使程序崩溃。但如果我们发现错误,我们会看到一些令人放心的东西:

      int main() {
          try {
              Basic * ptrs = new Basic[10];
              delete[] ptrs;
          } catch (std::runtime_error const& e) {std::cerr << e.what() << std::endl;}
          return 0;
      }
      

      输出变成这样:

      Default-Constructor
      Default-Constructor
      Default-Constructor
      Default-Constructor
      Default-Constructor
      Default-Constructor
      Destructor
      Destructor
      Destructor
      Destructor
      Destructor
      Oops!
      

      因此,即使没有显式的delete[] 调用,也会为完全构造的对象自动调用析构函数,因为new[] 调用具有处理这种情况的处理机制。

      但是你必须担心第六个对象:在我们的例子中,因为Basic 不做任何资源管理(如果它的构造函数可以的话,一个设计良好的程序不会让Basic 做资源管理像这样扔),我们不必担心。但是如果我们的代码看起来像这样,我们可能不得不担心:

      #include <iostream>
      
      struct Basic {
          Basic() { std::cout << "Default-Constructor" << std::endl; }
          Basic(Basic const&) { std::cout << "Copy-Constructor" << std::endl; }
          Basic(Basic &&) { std::cout << "Move-Constructor" << std::endl; }
          Basic & operator=(Basic const&) { std::cout << "Copy-Assignment" << std::endl; return *this; }
          Basic & operator=(Basic &&) { std::cout << "Move-Assignment" << std::endl; return *this; }
          ~Basic() noexcept { std::cout << "Destructor" << std::endl; }
      };
      
      class Wrapper {
          Basic * ptr;
      public:
          Wrapper() : ptr(new Basic) { 
              std::cout << "WRDefault-Constructor" << std::endl;
              static int val = 0;
              if(val++ == 5) throw std::runtime_error("Oops!");
          }
          Wrapper(Wrapper const&) = delete; //Disabling Copy/Move for simplicity
          ~Wrapper() noexcept { delete ptr; std::cout << "WRDestructor" << std::endl; }
      };
      
      int main() {
          try {
              Wrapper * ptrs = new Wrapper[10];
              delete[] ptrs;
          } catch (std::runtime_error const& e) {std::cout << e.what() << std::endl;}
          return 0;
      }
      

      在这里,我们得到这个输出:

      Default-Constructor
      WRDefault-Constructor
      Default-Constructor
      WRDefault-Constructor
      Default-Constructor
      WRDefault-Constructor
      Default-Constructor
      WRDefault-Constructor
      Default-Constructor
      WRDefault-Constructor
      Default-Constructor
      WRDefault-Constructor
      Destructor
      WRDestructor
      Destructor
      WRDestructor
      Destructor
      WRDestructor
      Destructor
      WRDestructor
      Destructor
      WRDestructor
      Oops!
      

      Wrapper 对象的大块不会泄漏内存,但第六个Wrapper 对象会泄漏一个Basic 对象,因为它没有被正确清理!


      幸运的是,就像任何资源管理方案通常的情况一样,如果您使用智能指针,所有这些问题都会消失:

      #include <iostream>
      #include<memory>
      
      struct Basic {
          Basic() { std::cout << "Default-Constructor" << std::endl; }
          Basic(Basic const&) { std::cout << "Copy-Constructor" << std::endl; }
          Basic(Basic &&) { std::cout << "Move-Constructor" << std::endl; }
          Basic & operator=(Basic const&) { std::cout << "Copy-Assignment" << std::endl; return *this; }
          Basic & operator=(Basic &&) { std::cout << "Move-Assignment" << std::endl; return *this; }
          ~Basic() noexcept { std::cout << "Destructor" << std::endl; }
      };
      
      class Wrapper {
          std::unique_ptr<Basic> ptr;
      public:
          Wrapper() : ptr(new Basic) { 
              std::cout << "WRDefault-Constructor" << std::endl;
              static int val = 0;
              if(val++ == 5) throw std::runtime_error("Oops!");
          }
          //Wrapper(Wrapper const&) = delete; //Copy disabled by default, move enabled by default
          ~Wrapper() noexcept { std::cout << "WRDestructor" << std::endl; }
      };
      
      int main() {
          try {
              std::unique_ptr<Wrapper[]> ptrs{new Wrapper[10]}; //Or std::make_unique
          } catch (std::runtime_error const& e) {std::cout << e.what() << std::endl;}
          return 0;
      }
      

      还有输出:

      Default-Constructor
      WRDefault-Constructor
      Default-Constructor
      WRDefault-Constructor
      Default-Constructor
      WRDefault-Constructor
      Default-Constructor
      WRDefault-Constructor
      Default-Constructor
      WRDefault-Constructor
      Default-Constructor
      WRDefault-Constructor
      Destructor
      WRDestructor
      Destructor
      WRDestructor
      Destructor
      WRDestructor
      Destructor
      WRDestructor
      Destructor
      WRDestructor
      Destructor
      Oops!
      

      请注意,对Destructor 的调用次数现在与对Default-Constructor 的调用次数相匹配,这告诉我们Basic 对象现在正在正确清理。而且由于Wrapper 正在执行的资源管理已委托给unique_ptr 对象,因此第六个Wrapper 对象没有调用其删除器这一事实不再是问题。

      现在,其中很多都涉及草编代码:即使通过使用智能指针使其“安全”,任何理性的程序员都不会拥有资源管理器throw 而没有正确处理代码。但是有些程序员就是不通情达理的,即使他们是通情达理的,你也可能会遇到一个奇怪的、奇异的场景,你必须编写这样的代码。因此,就我而言,教训是始终使用智能指针和其他 STL 对象来管理动态内存。不要试图自己动手。在尝试调试时,它会为您省去头疼的问题。

      【讨论】:

        猜你喜欢
        • 2012-01-29
        • 2023-03-27
        • 2013-01-01
        • 2011-06-11
        • 2014-05-27
        • 2011-04-03
        • 2010-12-16
        • 2012-04-15
        • 2019-02-10
        相关资源
        最近更新 更多