operator =重载链接列表问题

时间:2013-10-27 07:57:54

标签: c++ linked-list operator-overloading

我遇到链接列表的overloaded =运算符实现问题。 List类包含Node* head指针和包含struct NodeT* data的{​​{1}},其中T是模板类型名称。我遇到了运算符函数结束时发生的问题,其中析构函数(在本例中由Node* next处理)在运算符结束时被调用两次,一次在遍历列表并创建之后一个新的相同节点列表,一个在运算符函数退出后。

这是makeEmpty实现:

makeEmpty

这是operator = overload实现:

// Does the work of the Destructor
template <typename T>
void List<T>::makeEmpty() {

cout << endl << endl << "DESTRUCTOR CALLED" << endl << endl;
List<T>::Node* tempPtr = head;

if (head != NULL) {

    List<T>::Node* nextPtr = head->next;

    for(;;) {
        if (tempPtr != NULL) {

            delete tempPtr->data;
            tempPtr = nextPtr;

            if (nextPtr != NULL)
                nextPtr = nextPtr->next;

            /*tempPtr = head->next;
            delete head;
            head = tempPtr;*/
        }

        else break;
    }
}

}

我已经调试了一下,似乎第二次调用析构函数时,列表的头部被破坏包含不可读的内存。我甚至不确定为什么析构函数在运算符中被调用两次,因为唯一需要销毁的列表是在返回后的listToReturn。 (也许我的逻辑在某个地方存在缺陷......我一直在考虑这个问题太久了)

如果你们需要关于代码的更多信息,我很乐意提供。像往常一样,这是一项任务,所以我只是要求提供可以帮助我找到正确方向的提示。感谢大家的关注和帮助!

1 个答案:

答案 0 :(得分:4)

你问了指针和提示,所以我给它:

1)不必要的动态数据成员

您的List<T>::Node 需要基础数据值的动态成员。它应该可以从const T&构建,如果实现符合C ++ 11的移动构造习惯用法,也可以构造T&&两者都应将next成员初始化为nullptr

2)List<T>的复制构造函数是 mandetory

根据Rules of Three, Four, or Five,您的类具有动态成员,因此必须在复制构造和赋值操作符操作中正确管理它们(或隐藏所述实现,但显然不是你的选择,因为它是你的任务的一部分)。

3)利用赋值运算符重载的类复制构造函数

重载一个涉及动态分配的赋值运算符(它们在这里,因为你的Node的链接列表强制它)理想情况下应该使用copy-constructor和copy / swap惯用语进行生命周期管理。这有许多好处,最重要的两个可能是通过优化编译器忽略复制操作,以及将对象保持在其原始状态的异常安全性。

4)List<T>::operator =覆盖应该返回对当前对象的引用

赋值给的当前对象(运算符的左侧)应该是by-reference返回结果。它应该是被修改的对象。这是此类运营商的标准实施的一部分。你返回一个副本,但是原始对象保持不变,从而完全违背赋值运算符的目的(即,在你的实现中,左值侧实际上没有被修改)

以下详细说明了每一项:


不必要的动态数据成员

由于没有发布,我必须在List看起来有点理性。我想象这样的事情:

template<class T>
class List
{
private:
    struct Node
    {
        T* data;
        Node* next;
    };
    Node *head;

    // other members and decls...
};

有了这个,您的插入和复制操作必须具有显着的优势来管理他们不应该需要的T个对象的动态分配。 List<T>当然应该拥有Node链;但是Node应该拥有实际的T对象,并负责在其中进行管理; List<T>。请考虑一下:

template<class T>
class List
{
private:
    struct Node
    {
        T data;
        Node* next;

        Node(const T& arg) 
            : data(arg), next()
        {}

        Node(const Node& arg)
            : data(arg.data), next()
        {}

    private:
        // should never be called, and therefore hidden. A
        // C++11 compliant toolchain can use the `delete` declarator.
        Node& operator =(const Node&);
    };
    Node *head;

    // other members and decls...
};

现在,当需要一个新节点来保存T对象时(例如在插入操作中),可以执行以下操作:

template<typename T>
void List<T>::someFunction(const T& obj)
{ 
    Node *p = new Node(obj);
    // ...use p somewhere...
}

List<T>的复制构造函数 mandetory

您的链接列表就其本质而言,管理着一个动态链。因此,此类必须实现复制构造和赋值操作,后者是您的任务的一部分以及您发布问题的原因。复制链表是相当简单的,但是无论出于何种原因,一些链表都比它看起来更难。以下是我喜欢的方法之一:

template<typename T>
List<T>::List(const List<T>& arg)
    : head()
{
    Node **dst = &head;
    const Node* src = arg.head;
    while (src)
    {
        *dst = new Node(*src);     // invoke Node copy-construction
        dst = &(*dst)->next;       // move target to new node's next pointer
        src = src->next;           // advance source
    }
}

这使用一种指针指针的简单技术来保存指向下一个新节点填充的指针的地址。最初它保存了我们的头指针的地址。随着每个新节点的添加,它会被提前保存新添加的节点next成员的地址。由于Node(const Node&)已将next设置为nullptr(请参阅前一部分),我们的列表始终会正确终止。


利用赋值运算符重载的类复制构造函数

List<T>::operator =覆盖应返回对当前对象的引用

一旦我们有了一个可靠的拷贝构造函数,我们就可以用它来覆盖我们的赋值运算符。这是以一种不太明显的方式完成的,但我会在代码后解释:

template<typename T>
List<T>& List<T>::operator=(List<T> byval)
{
    std::swap(head, byval.head); // we get his list; he gets ours
    return *this;
}

我确定你正在考虑这个并思考,&#34;嗯??&#34;。值得一些解释。仔细查看传入的参数byval,并考虑为什么我像我一样命名它。 是您可能习惯看到的传统const引用。它是赋值表达式右侧的值 copy 。因此,要创建它,编译器将生成一个新的List<T>,调用copy-constructor来执行此操作。该副本的结果是我们作为参数的临时对象byval。我们所做的只是交换头指针。想想那是做什么的。通过交换头指针,我们拿出他的名单,然后拿走我们的名单。但是他是作业表达的原始右侧的副本,而我们的,我们希望它被删除。这正是在此函数完成后触发byval析构函数时会发生的情况。

简而言之,它会生成如下代码:

List<int> lst1, lst2;
lst1.insert(1);
lst2.insert(2);
lst1 = lst2; // <== this line

在标记的行上执行我们的功能。该函数将生成lst2副本,并将其传递给赋值运算符,其中lst1将与临时副本交换头指针。结果将是lst1的旧节点将由byval的析构函数清理,并且新节点列表已正确到位。

有很多理由这样做。首先,它使您的赋值运算符异常安全。如果抛出异常(通常是内存分配异常,但它并不重要),则不会泄漏内存,并且原始对象lst1仍保持其原始状态。其次,如果选择并且条件正确,编译器可以完全忽略它。

无论如何,这些是你的实现中的一些想法和一些错误点。我希望你发现它们很有用。