关于手动内存管理和深度复制的新手问题

时间:2009-01-22 05:32:03

标签: c++ memory-management deep-copy

好吧,所以我第一次尝试使用C ++,因为看起来我将不得不将它用于即将开设的大学课程。我有几年的编程经验,但在非垃圾收集的世界中并不多。

我有一个类,一个用于双向链表的节点。所以基本上它有一个值和两个指向其他节点的指针。主构造函数看起来像Node(const std::string & val, Node * prev, Node * next)。该练习包括一个复制构造函数,该复制构造函数执行另一个节点的浅表副本,其上方有一条注释,用于更改它以进行深层复制。

这就是我认为的意思:

Node(const Node & other)
      : value(other.value)
{
  prev = new Node(other.prev->value, other.prev->prev, other.prev->next);
  next = new Node(other.next->value, other.next->prev, other.next->next);
}

这似乎完成了制作它的目标,以便更改复制的节点不会影响新节点。但是,当我这样做时,我在堆上分配新东西。这让我很担心,因为我认为这也意味着我也应该在Node的析构函数中删除它。但是现在这与其他构造函数不一致,其中指向节点的指针只是传入,已经指向了某些东西。我无法在析构函数中delete next prev#include <string> //! Node implements a doubly-linked list node class Node { friend class LinkedList; //!< LinkedList can access private members of Node public: //! Constructor Node(const std::string & v, Node * p, Node * n) : value(v), prev(p), next(n) { } //! Change to deep copy Node(const Node & other) : value(other.value), prev(other.prev), next(other.next) { } //! Read-only public methods for use by clients of the LinkedList class const std::string & GetValue() const { return value; } Node * GetPrevious()const { return prev; } Node * GetNext()const { return next; } //! Change to deep copy Node & operator=(const Node & other) { if(this!=&other) { value=other.value; prev=other.prev; next=other.next; } return *this; } private: std::string value; //!< value stored in the node Node * prev; //!< pointer to previous node in the list Node * next; //!< pointer to next node in the list }; 继续进行,对吧?

我真的很困惑,指导赞赏!

编辑:这是代码(在我上面的更改之前),按要求:

{{1}}

5 个答案:

答案 0 :(得分:2)

通常“深层复制”涉及遍历数据结构并复制整个事物。在您的情况下,给定一个节点,制作一份完整的列表副本。

答案 1 :(得分:2)

深层复制制作结构的完整副本。我所说的结构是一组对象,它们协同工作来执行任务。如果你有一个汽车类,每个车轮和车身都有一个物体 - 一个深刻的副本可以复制整个汽车(并制作轮子和车身的副本)。

在您的情况下,“整个结构”是列表。只有在“列表级别”执行时,深度复制操作才有意义。节点的深层副本将复制节点指向的数据 - 但不会将其自身分配为列表的一部分(因为节点应该不知道“主”列表对象)。

List* List::CopyList()
{
    List* nlist = new List();
    ListNode* node = NULL, prev = NULL;
    ListNode* newNodes = new ListNode[m_nodeCount];
    int i = 0;
    while ((node == NULL && node = m_first) || (node = node->Next()) != NULL)
    {
        newNodes[i] = node->CopyNode(); // also makes a new copy of the node's data
        newNodes[i]->SetNext(NULL);
        newNodes[i]->SetPrev(prev);
        if (prev) prev->SetNext(newNodes[i]);
        prev = newNodes[i];
        ++i;
    }

    if (m_len > 0)
        nlist->SetFirst(newNodes[i]);
    if (m_len > 1)
        nlist->SetLast(newNodes[m_len - 1]);
    return nlist;
}

注意:我刚从我的屁股中取出代码,所以它没有经过测试。希望它有所帮助:))

答案 2 :(得分:2)

首先,我不确定如何理解练习的目标。副本有多深?在像您这样的解决方案中,this->next->nextother.next->next仍然是相同的。该对象是否应该重复?列表的其余部分?它在哪里结束?当然可以对整个列表进行深层复制,但我认为这对于单个节点的复制构造函数来说是一个非常意外的行为。

可能value成员变量是一个指针,应该被深层复制吗?这对我来说更有意义。

但回到你的解释:

Node a(...);
// ... more code that adds a whole list to a
Node b(a);

您的实施存在两个问题。对于一个b->next->prev点到a,我怀疑它应该指向b。其次,您需要考虑角落情况,其中a可能是列表中的第一个或最后一个节点。

对于你的主要问题:你当然是正确的,新创建的对象需要再次delete。无论您是复制prevnext节点还是整个列表,我都会说该副本的用户有责任再次删除所有复制的节点。我假设使用正常的,未复制的列表,该列表的用户将遍历所有节点并一旦他完成列表就一个接一个地手动删除它们。他不会假设一个节点的析构函数删除整个列表。复制品也是如此,它们应该表现相同。复制的东西的用户应该删除所有副本。 (实际上,您可能会有一个list类,它可以为您完成所有节点管理。)

但同样,如果节点的复制构造函数复制整个列表,甚至只复制其中的几个节点,这将是非常意外的,并且人们总是忘记清理所有这些副本。但这不是你的节点类的错,而是运动要求'。

答案 3 :(得分:2)

你担心的是正确的。

通过将指针传递给Node构造函数,没有关于使用指针传递的所有权的信息。这是构造函数的糟糕设计。您应该传入一个引用,表明您不拥有下一个节点或传递一个std :: auto_ptr&lt;&gt;这表明你必须拥有所有权。有人可能会说下一个或上一个可能是NULL(列表的开头或结尾),因此你不能使用引用,但这可以通过使用替代构造函数来克服。

当然也有例外:
Node类是否是另一个类的私有成员。如果是这种情况,Node类的使用完全由所有者控制,因此它的正确用法将由拥有类控制。

您没有提供的是析构函数的定义。有了这个,我们将能够判断节点是否实际上占用了传递给构造函数的指针(或者如果next和prev已经是智能指针)?

答案 4 :(得分:1)

如果每个节点都复制了它指向的节点,那么您可以安全地删除析构函数中的对象。如果你传递指针(作为构造函数Node(const std :: string&amp; v,Node * p,Node * n))那么你没有“拥有”指针而不应该删除它们。如果这是链表类的一部分,那么该类应该拥有指针并根据需要删除对象。您还可以使Node成为链表类的私有子类,以避免用户(或您自己)弄乱您的指针。

你在实现中的递归中也犯了一个错误,复制构造函数包含一个级别的深拷贝并调用“普通”构造函数,它接受指针,使其变浅。这意味着你的深度复制只有一个层次。它应该重复调用复制构造函数,如下所示:

Node(const Node & other) : value(other.value)
{
  prev = new Node(*(other.prev));
  next = new Node(*(other.next));
}

AFAIK这里使用深层拷贝没有任何好处,我能想到的唯一实际应用是复制整个列表,这可以在代表所述列表的类中更有效地处理。