链表:如何实现析构函数、复制构造函数和复制赋值运算符?

Posted

技术标签:

【中文标题】链表:如何实现析构函数、复制构造函数和复制赋值运算符?【英文标题】:Linked List: How to implement Destructor, Copy Constructor, and Copy Assignment Operator? 【发布时间】:2021-07-16 16:54:23 【问题描述】:

这是我的 C++ 代码:

#include <iostream>

using namespace std;

typedef struct Node
   
    int data;
    Node* next;
Node;

class LinkedList

private:
    Node* first;
    Node* last;
public:
    LinkedList() first = last = NULL;;
    LinkedList(int A[], int num);
    ~LinkedList();

    void Display();
    void Merge(LinkedList& b);
  
;

// Create Linked List using Array
LinkedList::LinkedList(int A[], int n)
   
    Node* t = new Node;
    if (t == NULL)
    
        cout << "Failed allocating memory!" << endl;
        exit(1);
    
    t->data = A[0];
    t->next = NULL;
    first = last = t;

    for (int i = 1; i < n; i++)
    
        t = new Node;
        if (t == NULL)
        
            cout << "Failed allocating memory!" << endl;
            exit(1);
        
        t->data = A[i];
        t->next = NULL;
        
        last->next = t;
        last = t;
    


// Deleting all Node in Linked List
LinkedList::~LinkedList()

    Node* p = first;
    Node* tmp;

    while (p != NULL)
    
        tmp = p;
        p = p->next;
        delete tmp;
    


// Displaying Linked List
void LinkedList::Display()

    Node* tmp;

    for (tmp = first; tmp != NULL; tmp = tmp->next)
        cout << tmp->data << " ";
    cout << endl;    


// Merge two linked list
void LinkedList::Merge(LinkedList& b)

    // Store first pointer of Second Linked List
    Node* second = b.first;
    Node* third = NULL, *tmp = NULL;

    // We find first Node outside loop, smaller number, so Third pointer will store the first Node
    // Then, we can only use tmp pointer for repeating process inside While loop
    if (first->data < second->data)
    
        third = tmp = first;
        first = first->next;
        tmp->next = NULL;
    
    else
    
        third = tmp = second;
        second = second->next;
        tmp->next = NULL;
    

    // Use while loop for repeating process until First or Second hit NULL
    while (first != NULL && second != NULL)
    
        // If first Node data is smaller than second Node data
        if (first->data < second->data)
        
            tmp->next = first;
            tmp = first;
            first = first->next;
            tmp->next = NULL;
        
        // If first Node data is greater than second Node data
        else
        
            tmp->next = second;
            tmp = second;
            second = second->next;
            tmp->next = NULL;
        
    

    // Handle remaining Node that hasn't pointed by Last after while loop
    if (first != NULL)
        tmp->next = first;
    else
        tmp->next = second;

    // Change first to what Third pointing at, which is First Node
    first = third;    

    // Change last pointer from old first linked list to new last Node, after Merge
    Node* p = first;
    while (p->next != NULL)
    
        p = p->next;
        
    last = p;
    
    // Destroy second linked list because every Node it's now connect with first linked list
    // This also prevent from Double free()
    b.last = NULL;
    b.first = NULL;


int main()

    int arr1[] = 4, 8, 12, 14, 15, 20, 26, 28, 30;
    int arr2[] = 2, 6, 10, 16, 18, 22, 24;
    int size1 = sizeof(arr1) / sizeof(arr1[0]);
    int size2 = sizeof(arr2) / sizeof(arr2[0]);
    
    LinkedList l1(arr1, size1);
    LinkedList l2(arr2, size2);

    l1.Display();
    l2.Display();
    
    // Merge two linked list, pass l2 as reference
    l1.Merge(l2);
    l1.Display();

    return 0;

我是 C++ 初学者,在这段代码中,我练习如何合并两个链表。这实际上非常有效。我已经按排序顺序成功合并了两个链表。

但是,有人说我应该在 C++ 上遵循三法则。其中实现:DestructorCopy ConstructorCopy Assignment Operator

我看过很多关于这个的视频。我确实理解这基本上是处理 Shallow Copy 尤其是当我们不希望两个不同的对象指向相同的内存地址时。但是,我的问题是,我仍然不知道如何在一个类上实现它,就像我上面的代码一样。

有人说,在我的main() 中,这个代码:l1.Merge(l2); 是不正确的,因为我没有明确的复制构造函数。

如果您查看我的 Merge() 函数,在最后一行,如果我没有这样做:b.last = NULL;b.first = NULL;,它们只会破坏第二个链接列表的指针,编译器会给我警告:检测到双释放()。

所以,我想我的问题是:

    这段代码:l1.Merge(l2); 怎么可能与复制构造函数有关? Double free() 的发生是因为我没有实施三法则吗?如果是,如何解决? 如何根据我的代码编写三法则?何时或如何使用它们? 根据本准则,有什么问题吗?如果我的程序只想合并链表,我还需要三法则吗?

谢谢。我希望有人能像我10岁一样向我解释。并希望有人可以给我写一些代码。

【问题讨论】:

如何根据我的代码编写三法则?何时或如何使用它们? 任何时候你的类分配它拥有的内存并使用原始指针,你都需要遵循 3 或 5 的规则。 Double free() 发生是因为我没有实施三法则吗? 是的,double free() 是不实施的可能结果3 的规则。 @Kevinkun l1 = l2; -- 和 -- LinkedList l3 = l1; -- 你试过那个简单的测试吗?您的main 程序似乎避免了实际测试复制和分配。只需编写这些测试行,您就会看到事情崩溃或正常工作。 一个可能的实现是在你不需要的时候明确delete复制ctor和赋值操作符。这样,如果您的代码稍后尝试使用它们,您将收到编译时错误。当您在merge销毁 l2 时,我会使用&amp;&amp; 引用来明确说明。 你必须实现析构函数。您不必实现另外两个,您可以选择将它们设为deleted,在这种情况下,您的链接列表将是不可复制的(如果您不小心尝试复制它们,编译器会对您大喊大叫)。 【参考方案1】:

但是,我的问题是,我仍然不知道如何在一个使用链接列表的类上实现 [三规则],就像我上面的代码一样。

您只需实现复制构造函数和复制赋值运算符来迭代输入列表,制作每个节点的副本并将它们插入到目标列表中。你已经有一个工作的析构函数。在复制赋值运算符的情况下,您通常可以使用copy-swap idiom 来实现它,使用avoid repeating yourself 的复制构造函数。

有人说,在我的main() 中,这个代码:l1.Merge(l2); 是不正确的,因为我没有明确的复制构造函数。

那你被告知错了。您的Merge() 代码与复制构造函数无关

如果您查看我的 Merge() 函数,在最后一行中,如果我没有这样做:b.last = NULL;b.first = NULL;,它们只会破坏第二个链接列表的指针,编译器会给我警告:@ 987654330@

正确。由于您将节点从输入列表移动到目标列表,因此您需要重置输入列表,使其不再指向移动的节点。否则,输入列表的析构函数会尝试释放它们,目标列表的析构函数也会尝试释放它们。

这段代码:l1.Merge(l2); 怎么可能与复制构造函数有关?

与此无关。

Double free() 的发生是因为我没有执行三法则吗?

在您的特定示例中没有,因为您没有执行任何复制操作。但是,一般来说,不执行三法则会导致双重释放,是的。

如何根据我的代码编写三法则?

请看下面的代码。

如果我的程序只想合并链表,我还需要三法则吗?

没有。仅当您想要复制列表时。

话虽如此,下面是一个包含三法则的实现:

#include <iostream>
#include <utility>

struct Node

    int data;
    Node *next;
;

class LinkedList

private:
    Node *first;
    Node *last;
public:
    LinkedList();
    LinkedList(const LinkedList &src);
    LinkedList(int A[], int num);
    ~LinkedList();

    LinkedList& operator=(const LinkedList &rhs);

    void Display() const;
    void Merge(LinkedList &b);
;

// Create Linked List using default values
LinkedList::LinkedList()
    : first(NULL), last(NULL)



// Create Linked List using Array
LinkedList::LinkedList(int A[], int n)
    : first(NULL), last(NULL)

    Node **p = &first;

    for (int i = 0; i < n; ++i)
    
        Node *t = new Node;
        t->data = A[i];
        t->next = NULL;

        *p = t;
        p = &(t->next);

        last = t;
    


// Create Linked List by copying another Linked List
LinkedList::LinkedList(const LinkedList &src)
    : first(NULL), last(NULL)

    Node **p = &first;

    for (Node *tmp = src.first; tmp; tmp = tmp->next)
    
        Node* t = new Node;
        t->data = tmp->data;
        t->next = NULL;

        *p = t;
        p = &(t->next);

        last = t;
    


// Deleting all Node in Linked List
LinkedList::~LinkedList()

    Node *p = first;

    while (p)
    
        Node *tmp = p;
        p = p->next;
        delete tmp;
    


// Update Linked List by copying another Linked List
LinkedList& LinkedList::operator=(const LinkedList &rhs)

    if (&rhs != this)
    
        LinkedList tmp(rhs);
        std::swap(tmp.first, first);
        std::swap(tmp.last, last);
    
    return *this;


// Displaying Linked List
void LinkedList::Display() const

    for (Node *tmp = first; tmp; tmp = tmp->next)
        std::cout << tmp->data << " ";
    std::cout << std::endl;


// Merge two linked list
void LinkedList::Merge(LinkedList& b)

    if ((&b == this) || (!b.first))
        return;

    if (!first)
    
        first = b.first; b.first = NULL;
        last = b.last; b.last = NULL;
        return;
    

    // Store first pointer of Second Linked List
    Node *second = b.first;
    Node *third, **tmp = &third;

    // We find first Node outside loop, smaller number, so Third pointer will store the first Node
    // Then, we can only use tmp pointer for repeating process inside While loop
    // Use while loop for repeating process until First or Second hit NULL
    do
    
        // If first Node data is smaller than second Node data
        if (first->data < second->data)
        
            *tmp = first;
            tmp = &(first->next);
            first = first->next;
        
        // If first Node data is greater than second Node data
        else
        
            *tmp = second;
            tmp = &(second->next);
            second = second->next;
        
        *tmp = NULL;
    
    while (first && second);

    // Handle remaining Node that hasn't pointed by Last after while loop
    *tmp = (first) ? first : second;

    // Change first to what Third pointing at, which is First Node
    first = third;  

    // Change last pointer from old first linked list to new last Node, after Merge
    Node *p = first;
    while (p->next)
    
        p = p->next;
       
    last = p;
    
    // Destroy second linked list because every Node it's now connect with first linked list
    // This also prevent from Double free()
    b.first = b.last = NULL;


int main()

    int arr1[] = 4, 8, 12, 14, 15, 20, 26, 28, 30;
    int arr2[] = 2, 6, 10, 16, 18, 22, 24;
    int size1 = sizeof(arr1) / sizeof(arr1[0]);
    int size2 = sizeof(arr2) / sizeof(arr2[0]);
    
    LinkedList l1(arr1, size1);
    LinkedList l2(arr2, size2);
    LinkedList l3(l1);
    LinkedList l4;

    l1.Display();
    l2.Display();
    l3.Display();
    l4.Display();
    
    // Merge two linked list, pass l2 as reference
    l3.Merge(l2);
    l4 = l3;

    l1.Display();
    l2.Display();
    l3.Display();
    l4.Display();

    return 0;

Demo

【讨论】:

哇。谢谢你的解释! 那么,对于这个函数:LinkedList::LinkedList(const LinkedList &amp;src)LinkedList&amp; LinkedList::operator=(const LinkedList &amp;rhs) 只有当我在我的代码上执行从现有对象复制到新对象时才会执行?喜欢l1 = l2LinkedList l2(l1) 是的,它们是复制构造函数和复制赋值运算符,它们仅在复制时调用。如果您真的想要彻底,请考虑五法则,它添加了一个移动构造函数和移动赋值运算符,以窃取资源而不制作副本。移动语义是在 C++11 中引入的【参考方案2】:

这段代码中应用了几个有问题的做法,也有一个bug。

首先,错误。当您创建一个列表时,它会news 其所有节点并使用指针跟踪它们。当您将一个列表分配给另一个列表时,您实际上是在复制指针值。您现在不仅丢失了分配列表的节点(因为您覆盖了它们)并且出现内存泄漏(因为现在没有指向分配节点的指针),您现在在两个不同的列表上也有相同的指针,指向相同的节点。当列表被销毁时,他们都尝试delete他们的节点,你最终释放了两次相同的内存。呵呵。

这个bug的解决方法是实现赋值运算符。

然后,有问题的做法:

    using namespace std; (Why is "using namespace std;" considered bad practice?) 您在构造函数主体中分配LinkedList 的成员,而不是在初始化列表中将值直接传递给它们的构造函数。 (What is this weird colon-member (" : ") syntax in the constructor?) 声明一个数组参数 (int[]) 就是声明一个指针。请注意这一点。 new 无法返回 NULL!检查它的返回值是没有用的。如果不能分配,它只会抛出一个异常。 NULL 是不适合使用的常量。您可以使用 nullptr,它是 NULL 的 C++ 等效项,但它是类型安全的。 使用newdelete 进行手动内存管理很难正确处理(正如您自己发现的那样)。您可能有兴趣使用std::unique_ptrstd::shared_ptr 来减轻负担。他们会发现这个错误的。

现在,请:不要用 C++ 编写,就像使用带有类的 C 一样。我知道您可能没有遇到我在这里介绍的所有功能,但无论如何现在您都知道它们了:)

【讨论】:

感谢您的可疑做法!我理解您对内存泄漏的解释。但是,哪个创建代码是错误的?你的意思是这个创建代码是错误的:LinkedList l1(arr1, size1);LinkedList l2(arr2, size2); 吗?以及我必须将哪些代码与赋值运算符一起使用? 因为它的代码实际上可以完美运行。我通过按引用传递来处理内存泄漏,并通过分配b.first = NULLb.last=NULL 来销毁l2,因此当调用析构函数时,它不会双重释放。我的问题是,我想知道我的代码是否有使用三法则的好习惯? 对于评论 1:我所说的错误发生在您分配 LinkedList 时。即使您没有定义赋值运算符,编译器也为您定义了一个恰好做错事的运算符。 (注意:您没有在发布的代码中使用赋值运算符。这就是为什么一些 cmets 建议将其标记为 deleted (与内存释放 btw 完全不同的东西),因此它永远不会被调用。然而,错误仍然存​​在在那里,它只是没有展示自己)。 对于评论 2:当您不能遵循零规则时,使用三规则通常是一种很好的做法。也就是说,当编译器提供构造函数和赋值运算符时,不会做你想做的事。这里的底线是你写的代码越少越好,所以如果可以的话让编译器为你写。 已编辑: 答案部分错误,因为我以为您在谈论代码的其他部分。 另外,这里有个提示:当你 new 时,你也可以调用 Node 的构造函数。编译器为您生成了一个在这种情况下执行您想要的操作(查找聚合初始化)。

以上是关于链表:如何实现析构函数、复制构造函数和复制赋值运算符?的主要内容,如果未能解决你的问题,请参考以下文章

构造/析构/赋值运算

❥关于C++之类的复制构造函数&赋值运算符

C++类的成员函数:构造析构拷贝构造运算符重载

C++类的成员函数:构造析构拷贝构造运算符重载

C++类的成员函数:构造析构拷贝构造运算符重载

C++类的成员函数:构造析构拷贝构造运算符重载