【问题标题】:C++ destructor issue with std::vector of class objects类对象的 std::vector 的 C++ 析构函数问题
【发布时间】:2011-02-14 06:41:33
【问题描述】:

当我有一个类的 std::vector 时,我对如何使用析构函数感到困惑。

所以如果我创建一个简单的类如下:

class Test
{
private:
 int *big;

public:
 Test ()
 {
  big = new int[10000];
 }

    ~Test ()
 {
  delete [] big;
 }
};

然后在我的主要功能中执行以下操作:

Test tObj = Test();
vector<Test> tVec;
tVec.push_back(tObj);

当我超出范围时,我在 Test 的析构函数中遇到运行时崩溃。为什么会这样?如何安全地释放我的内存?

【问题讨论】:

  • 不知何故,我的 DevC++ 将我从这些困境中“救了出来”。此代码 [ideone.com/EHZBV] 在 Windows 上的 DevC++ 上执行得非常好。任何想法为什么会发生这种情况?这是不好的。我希望它在它应该崩溃的时候崩溃!

标签: c++ memory vector destructor


【解决方案1】:

问题是您没有为Test 定义复制构造函数。所以编译器会为你生成一个默认的复制构造函数,它只是复制对象的内容——在本例中是 int 指针。

现在,当您将对象推回向量中时,它会被复制构造函数隐式复制。这导致两个对象指向同一个整数数组!所以最后,两个析构函数尝试删除同一个数组 - BANG。

每当你定义一个通过指针*拥有成员的类时,除了析构函数之外,你必须还为它定义一个复制构造函数。 更新: 和赋值运算符,出于同样的原因(感谢@James :-)

更新2:解决所有这些限制的简单方法是定义一个静态数组而不是动态分配的数组:

class Test
{
private:
  int big[10000];
  // no need for constructors, destructor or assignment operator
};

但是,最佳做法是使用 std::vector&lt;int&gt; 而不是数组。

* 即包含指向具有所有权语义的成员的指针(感谢@Steve Jessop 的澄清)

【讨论】:

  • 没错。当测试对象被添加到向量时,它在复制构造函数的帮助下被复制。如果您也尝试访问向量对象的数组,您应该会崩溃。
  • “每当你定义一个包含指针成员的类” - 具有所有权语义。没有所有权语义的指针不需要类的特殊处理,但当然,调用者必须确保引用对象在对象可能使用它时保持有效。
  • @Steve,对我来说,收容意味着所有权,但你是对的,我现在试图让它更清楚。
【解决方案2】:

你的问题在这里:

Test tObj = Test();

Test() 创建一个临时的Test 对象,然后将其复制到tObj。此时,tObj 和临时对象都将big 设置为指向数组。然后临时对象被销毁,它调用析构函数并销毁数组。所以当tObj被销毁时,它会再次尝试销毁已经销毁的数组。

另外,当tVec被销毁时,它会销毁它的元素,所以已经销毁的数组会再次被销毁。

您应该定义一个复制构造函数和一个赋值运算符,以便在复制 Test 对象时,复制 big 数组,或者具有某种引用计数,以便在所有所有者之前它不会被销毁被销毁了。

一个简单的解决方法是像这样定义你的类:

class Test
{
private:
 std::vector<int> big;

public:
 Test (): big(10000) {}
};

在这种情况下,您不需要定义任何析构函数、复制构造函数或赋值运算符,因为std::vector&lt;&gt; 成员会处理所有事情。 (但请注意,这意味着每当您复制 Test 的实例时,都会分配和复制 10,000 个字节。)

【讨论】:

  • 原则上正确但细节不正确。 Test tObj = Test() 不会创建临时对象。在 tVec.push_back(tObj); 处调用复制构造函数;
  • @shura:根据定义,C++ 中的表达式Test() 创建一个临时对象。允许编译器对其进行优化,但一般情况下会创建临时的。
  • @AndreyT:我不明白你的“定义”。您自己说允许编译器对其进行优化。他们做到了。那个时候定义会中断吗?您使用哪个编译器调用 Test o = Test() 的复制构造函数?
  • @shura:首先,当我说“定义”时,我指的是Test() 子表达式。这个子表达式创建了一个Test 类型的临时对象,定义见5.2.3。其次,整个 C++ 中的 Test tObj = Test() 字面意思是“创建一个临时文件并使用复制构造函数复制它”。大多数编译器会决定优化它,因为它们被明确允许,但这是一个完全不同的故事。此外,如果 所有 编译器决定不优化代码(由于编译器设置或上下文等),它们将创建一个临时的。
  • 我是否知道实际执行复制的编译器并不重要。重要的是语言规范所说的内容。最后,所有编译器(除非它们被破坏)将需要可访问的复制构造函数来编译Test tObj = Test(),因为从概念上讲,代码执行临时的复制,即使实际复制被优化掉了。我不明白你关于“打破定义”的观点。优化总是违反抽象的 C++ 行为,这就是它们被称为优化的原因。从这个意义上说,它们确实打破各种定义。
【解决方案3】:

如果没有复制构造函数,向量将创建对象的平面副本。这导致两个Test 类型的对象引用相同的数组big。第一个实例在数组被销毁时将其删除,然后第二个实例尝试取消引用已删除的指针,这是未定义的行为。

【讨论】:

    【解决方案4】:
    Test tObj = Test();
    

    这是错误的,应该是因为它不会创建副本:

    Test tObj;
    

    这也会创建很多副本:

     vector<Test> tVec;
     tVec.push_back(tObj);
    

    因此,如果您释放一个 int 数组,您将释放所有数组。并且下面的删除会失败。

    你需要的是:

    • 使用复制构造函数为每个类拥有一个单独的数组

    • 为什么要使用指针?

    class Test    
    {   
    private:  
      int big[10000];    
    public:
    
    };
    

    这会很好用。

    【讨论】:

    • 您的建议通常可以正常工作,但在某些平台上将int[10000] 放在堆栈上会导致问题。从堆中分配大对象通常更安全。
    • 很难确切地知道原始发布者使用的是哪个平台,但如果他真的知道为什么在这种情况下必须使用堆分配,也许他不会犯以下错误。好吧,我不知道,也许他毕竟应该只使用 std::vector 吗?当然,您可以只创建(智能)Test 指针来避免所有那些不必要的副本
    猜你喜欢
    • 2016-11-04
    • 1970-01-01
    • 2012-03-15
    • 1970-01-01
    • 1970-01-01
    • 2018-05-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多