53
#include <queue>
using namespace std;

class Test{
    int *myArray;

        public:
    Test(){
        myArray = new int[10];
    }

    ~Test(){
        delete[] myArray;
    }

};


int main(){
    queue<Test> q
    Test t;
    q.push(t);
}

运行此程序后,我收到运行时错误“双重释放或损坏”。如果我摆脱析构函数内容(delete),它工作正常。怎么了?

4

6 回答 6

87

让我们谈谈在 C++ 中复制对象。

Test t;,调用默认构造函数,它分配一个新的整数数组。这很好,你的预期行为。

当您t使用q.push(t). 如果您熟悉 Java、C# 或几乎任何其他面向对象的语言,您可能希望将您之前创建的对象添加到队列中,但 C++ 不是这样工作的。

当我们查看std::queue::pushmethod时,我们会看到添加到队列中的元素“初始化为 x 的副本”。它实际上是一个全新的对象,它使用复制构造函数复制原始Test对象的每个成员以创建一个新的Test.

默认情况下,您的 C++ 编译器会为您生成一个复制构造函数!这非常方便,但会导致指针成员出现问题。在您的示例中,请记住这int *myArray只是一个内存地址;当 的值myArray从旧对象复制到新对象时,您现在将有两个对象指向内存中的同一个数组。这本质上并不是坏事,但是析构函数会尝试删除同一个数组两次,因此会出现“双重释放或损坏”运行时错误。

我如何解决它?

第一步是实现一个复制构造函数,它可以安全地将数据从一个对象复制到另一个对象。为简单起见,它可能看起来像这样:

Test(const Test& other){
    myArray = new int[10];
    memcpy( myArray, other.myArray, 10 );
}

现在,当您复制 Test 对象时,将为新对象分配一个新数组,并且该数组的值也将被复制。

不过,我们还没有完全摆脱麻烦。编译器为您生成的另一种方法可能会导致类似的问题 - 分配。不同之处在于,通过赋值,我们已经有了一个需要适当管理其内存的现有对象。这是一个基本的赋值运算符实现:

Test& operator= (const Test& other){
    if (this != &other) {
        memcpy( myArray, other.myArray, 10 );
    }
    return *this;
}

这里重要的部分是我们将数据从另一个数组复制到这个对象的数组中,保持每个对象的内存分开。我们还检查了自我分配;否则,我们会从自己复制到自己,这可能会引发错误(不确定它应该做什么)。如果我们要删除并分配更多内存,自分配检查会阻止我们删除需要从中复制的内存。

于 2012-12-28T02:37:51.047 回答
17

问题是您的类包含一个托管的 RAW 指针,但没有实现三规则(C++ 11 中的五)。结果,由于复制,您(预期)得到了双重删除。

如果你正在学习,你应该学习如何实施三(五)规则。但这不是解决这个问题的正确方法。您应该使用标准容器对象,而不是尝试管理自己的内部容器。确切的容器将取决于您要执行的操作,但 std::vector 是一个很好的默认值(如果不是最佳的,您可以更改后记)。

#include <queue>
#include <vector>

class Test{
    std::vector<int> myArray;

    public:
    Test(): myArray(10){
    }    
};

int main(){
    queue<Test> q
    Test t;
    q.push(t);
}

您应该使用标准容器的原因是separation of concerns. 您的课程应该关注业务逻辑或资源管理(而不是两者)。假设Test您正在使用某个类来维护程序的某些状态,那么它是业务逻辑,它不应该进行资源管理。另一方面,如果Test应该管理一个数组,那么您可能需要更多地了解标准库中可用的内容。

于 2012-12-28T06:02:52.930 回答
4

您将获得双重释放或损坏,因为第一个析构函数用于对象q在这种情况下,由new分配的内存将是空闲的。下一次为对象t调用析构函数时,内存已经空闲(为 q 完成)因此当在析构函数中delete[] myArray; 将执行它会抛出双重释放或损坏。原因是两个对象共享相同的内存,因此定义了 \copy、赋值和相等运算符,如上述答案中所述。

于 2012-12-28T02:30:20.547 回答
3

你需要定义一个拷贝构造函数、赋值、操作符。

class Test {
   Test(const Test &that); //Copy constructor
   Test& operator= (const Test &rhs); //assignment operator
}

推送到队列中的副本指向与原始内存相同的内存。当第一个被破坏时,它会删除内存。第二个破坏并尝试删除相同的内存。

于 2012-12-28T02:23:14.973 回答
0

您还可以尝试在删除之前检查 null 这样

if(myArray) { delete[] myArray; myArray = NULL; }

或者您可以像这样以安全的方式定义所有删除操作:

#ifndef SAFE_DELETE
#define SAFE_DELETE(p) { if(p) { delete (p); (p) = NULL; } }
#endif

#ifndef SAFE_DELETE_ARRAY
#define SAFE_DELETE_ARRAY(p) { if(p) { delete[] (p); (p) = NULL; } }
#endif

然后使用

SAFE_DELETE_ARRAY(myArray);
于 2015-08-18T07:23:09.840 回答
-2

嗯,析构函数不应该调用delete,而不是delete[]?

于 2012-12-28T02:45:09.667 回答