0

我在为链表实现重载 = 运算符时遇到问题。List 类包含一个Node* head指针和一个struct Node包含T* dataand Node* next,其中 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 。(也许我的逻辑在某个地方有缺陷......我一直在考虑这个问题太久了)

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

4

1 回答 1

4

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

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

List<T>::Node不需要基础数据值的动态成员它应该可以从 a 构造,const T&并且如果实现符合 C++11 的移动构造习语, aT&&也是如此。两者next应该将成员初始化为nullptr

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

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

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

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

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

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

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


不必要的动态数据成员

由于它没有发布,我必须对它的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&)已经设置nextnullptr(参见上一节),我们的列表总是正确终止。


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

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

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

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

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

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

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

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

于 2013-10-27T09:32:29.343 回答