【问题标题】:operator= overload for Linked List issuesoperator= 链接列表问题的重载
【发布时间】:2013-11-06 02:35:50
【问题描述】:

我在为链表实现重载 = 运算符时遇到问题。 List 类包含一个Node* head 指针和一个包含T* data 和Node* next 的struct Node,其中T 是模板类型名。我对操作符函数末尾发生的事情有疑问,其中析构函数(在本例中由makeEmpty 处理)在操作符末尾被调用两次,一次是在遍历列表并创建一个新的之后相同节点的列表,并且在运算符函数退出后一次。

这里是 makeEmpty 的实现:

// 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;
    }
}

}

这里是 operator= 重载实现:

// Overloaded to be able to assign one list to another
template <typename T>
List<T> List<T>::operator=(const List& listToCopy) {

List<T> listToReturn;
listToReturn.head = NULL;
List<T>::Node* copyPtr = listToCopy.head;

List<T>::Node* thisPtr = head;

if (copyPtr != NULL && thisPtr != NULL) {
    for(;;) {
        if (copyPtr != NULL) {

            T* toInsert = new T(*copyPtr->data);
            listToReturn.insert(toInsert);

            copyPtr = copyPtr->next;
        }

        else{cout << endl << listToReturn << endl << endl; return listToReturn;}
    }
}
// if right-hand list is NULL, return an empty list
return listToReturn;
}

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

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

【问题讨论】:

  • 你和this questioner 真的需要聚在一起。并在您的问题中包含List 的 full 模板类声明。我可以看到您已经拥有不需要的一级动态分配。 List&lt;T&gt;::Node::data 完全没有理由是动态的,除非你能想到一个很好的理由来分配一个没有数据的 Node(顺便说一句,没有一个)。
  • 你应该在赋值运算符中返回List&lt;T&gt;&amp;

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


【解决方案1】:

你要求指点和提示,所以我给它:

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

您的List&lt;T&gt;::Node 不需要底层数据值的动态成员。它应该可以从 const T&amp; 构造,如果实现符合 C++11 的移动构造习语,则还可以使用 T&amp;&amp;。 两者都应该将next成员初始化为nullptr

2) List&lt;T&gt; 的复制构造函数是mandetory

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

3) 使用类复制构造函数进行赋值运算符重载

在涉及动态分配的情况下重载赋值运算符(它们在这里,因为您的Node 的链接列表要求它)应该理想地利用复制构造函数和复制/交换习语进行生命周期管理。这有很多好处,其中最重要的两个是通过优化编译器潜在地省略了复制操作,以及将对象保持在其原始状态的异常安全性。

4) List&lt;T&gt;::operator = 覆盖应该返回对当前对象的引用

当前被赋值给的对象(操作符的左边)应该是一个按引用返回的结果。它应该是被修改的对象。这是此类运算符的标准实现的一部分。您返回一个副本,但原始对象保持不变,从而完全违背了赋值运算符的目的(即,在您的实现中实际上根本没有修改左值端)

下面将详细介绍其中的每一个:


不必要的动态数据成员

由于它没有发布,我必须对List 的样子有些预言。我想像这样:

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

    // other members and decls...
};

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

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&lt;T&gt; 的复制构造函数是 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&amp;) 已经将next 设置为nullptr(请参阅上一节),我们的列表总是会正确终止。


使用类复制构造函数进行赋值运算符重载

List&lt;T&gt;::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;
}

我确定您看到这个并在想,“嗯??”。它值得一些解释。仔细看看传入的参数byval,想想我为什么这么命名。它不是您可能习惯看到的传统const 引用。它是赋值表达式右侧的值copy。因此,要创建它,编译器将生成一个新的List&lt;T&gt;,并调用复制构造函数来执行此操作。该副本的结果是我们作为参数的临时对象byval。我们所做的只是交换头指针。想想那是做什么的。通过交换头部指针,我们获取他的列表,而他获取我们的。但他是赋值表达式原始右侧的副本,而我们的,好吧,我们希望将其删除。这正是当函数完成后byval 的析构函数 被触发时会发生的情况。

简而言之,它使代码如下:

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

在标记的行上执行我们的函数。该函数将生成lst2 的副本,并将其传递给赋值运算符,lst1 将与临时副本交换头指针。结果会是lst1的旧节点会被byval的析构函数清理干净,新的节点列表就到​​位了。

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

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

【讨论】:

    猜你喜欢
    • 2020-01-12
    • 2020-08-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多