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

Linked List: How to implement Destructor, Copy Constructor, and Copy Assignment Operator? [closed]

提问人:Kevinkun 提问时间:7/17/2021 最后编辑:Kevinkun 更新时间:7/17/2021 访问量:1264

问:


想改进这个问题吗?通过编辑这篇文章来更新问题,使其仅关注一个问题。

去年关闭。

这是我的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++的三法则。实现:析构函数、复制构造函数复制赋值运算符

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

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

如果你看一下我的函数,在最后一行,如果我没有这样做:和,它只是破坏了第二个链表的指针,编译器会给我警告:检测到双重自由()。Merge()b.last = NULL;b.first = NULL;

所以,我想我的问题是:

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

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

C++ 链表 三法则

评论

0赞 drescherjm 7/17/2021
如何根据我的代码编写三法则?何时或如何使用它们?每当您的类分配它拥有的内存并使用原始指针时,您都需要遵循 3 或 5 的规则。
0赞 drescherjm 7/17/2021
Double free() 是因为我没有实现三法则而发生的吗?是的,双重释放是不实施 3 规则的可能结果。
1赞 PaulMcKenzie 7/17/2021
@Kevinkun -- 还有-- -- 你试过那个简单的测试吗?您的程序似乎避免了实际测试复制和分配。通过简单地编写这些测试行,您会看到事情分崩离析或正常工作。l1 = l2;LinkedList l3 = l1;main
1赞 Serge Ballesta 7/17/2021
一种可能的实现方式是,如果不需要复制 ctor 和赋值运算符,则仅显式删除它们。这样,如果您的代码稍后尝试使用它们,您将收到编译时错误。当你在 中销毁时,我会使用引用来明确它。l2merge&&
1赞 n. m. could be an AI 7/17/2021
您必须实现析构函数。你不必实现其他两个,你可以选择删除它们,在这种情况下,你的链表将是不可复制的(如果你不小心试图复制它们,编译器会对你大喊大叫)。

答:

0赞 super 7/17/2021 #1

此代码中应用了几种有问题的做法,并且还存在一个错误。

首先,错误。当您创建列表时,它会包含其所有节点,并使用指针跟踪它们。当您将一个列表分配给另一个列表时,您实际上会复制指针值。您现在不仅丢失了已分配列表的节点(因为您覆盖了它们)并发生了内存泄漏(因为现在没有指向已分配节点的指针),您现在还在两个不同的列表中拥有相同的指针,指向相同的节点。当列表被销毁时,它们都会尝试访问它们的节点,而您最终会释放相同的内存两次。育。newdelete

此 bug 的解决方案是实现赋值运算符。

然后,有问题的做法:

  1. using namespace std; (为什么“使用命名空间标准”被认为是不好的做法?)
  2. 在构造函数正文中分配 的成员,而不是将值直接传递给初始化列表中的构造函数。(构造函数中这个奇怪的冒号成员 (“ : ”) 语法是什么?LinkedList)
  3. 声明数组参数 () 就是声明指针。只要注意它。int[]
  4. new不能再来了!检查其返回值是没有用的。如果它不能分配,它只会抛出异常。NULL
  5. NULL是要使用的不合适的常量。您可以使用 ,它是 C++ 的等效项,但它是类型安全的。nullptrNULL
  6. 手动内存管理是很棘手的(正如你自己发现的那样)。您可能有兴趣使用或减轻负担。他们会抓住这个错误。newdeletestd::unique_ptrstd::shared_ptr

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

评论

0赞 Kevinkun 7/17/2021
谢谢你有问题的做法!我理解你对内存泄漏的解释。但是,哪个创建代码是错误的?你的意思是这个创建代码是错误的:和?以及我究竟必须将哪些代码与赋值运算符一起使用?LinkedList l1(arr1, size1);LinkedList l2(arr2, size2);
0赞 Kevinkun 7/17/2021
因为它的代码实际上运行良好。我通过引用传递和分配销毁来处理内存泄漏,因此当析构函数调用时,它不会双重释放。我的问题是,我想知道使用三法则是否可以在我的代码上有一个好的做法?l2b.first = NULLb.last=NULL
0赞 super 7/17/2021
对于评论 1:我所说的错误发生在您分配 .即使你没有定义赋值运算符,编译器也为你定义了一个恰好做错了事情的赋值运算符。(注意:您没有在发布的代码中使用赋值运算符。这就是为什么一些评论建议将其标记为 d(顺便说一句,这与内存释放完全不同),因此它永远不会被调用。然而,错误仍然存在,它只是没有出现)。LinkedListdelete
0赞 super 7/17/2021
对于注释 2:当您无法遵循零规则时,使用三法则通常是很好的做法。也就是说,当编译器提供构造函数和赋值运算符时,不会执行您想要的操作。这里的底线是你写的代码越少越好,所以如果可以的话,让编译器为你写。编辑:答案部分错误,因为我认为你在谈论代码的其他部分。
0赞 super 7/17/2021
另外,这里有一个提示:你也可以调用构造函数 ,当你的时候。编译器会为您生成一个,在这种情况下可以执行您想要的操作(查找聚合初始化)。Nodenew
0赞 Remy Lebeau 7/17/2021 #2

但是,因为我的问题是,我仍然不知道如何在一个在链表上工作的类上实现 [三分法则],就像我上面的代码一样。

您只需实现复制构造函数和复制赋值运算符即可迭代输入列表,创建每个节点的副本并将它们插入到目标列表中。您已经有一个工作析构函数。对于复制赋值运算符,通常可以使用复制交换习语通过复制构造函数来实现它,以避免重复自己

有人说,在我的 ,这段代码:在某种程度上是不正确的,因为我没有显式的复制构造函数。main()l1.Merge(l2);

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

如果你看一下我的函数,在最后一行,如果我没有这样做:和,它只是破坏了第二个链表的指针,编译器会给我警告:Merge()b.last = NULL;b.first = NULL;Double free() detected.

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

这个代码怎么可能与复制构造函数有关?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;
}

演示

评论

0赞 Kevinkun 7/17/2021
哇。谢谢你的解释!
0赞 Kevinkun 7/17/2021
那么,对于这个函数:只有当我在代码上执行从现有对象复制到新对象时,这才会执行?喜欢 或 ?LinkedList::LinkedList(const LinkedList &src)LinkedList& LinkedList::operator=(const LinkedList &rhs)l1 = l2LinkedList l2(l1)
0赞 Remy Lebeau 7/17/2021
是的,这些是复制构造函数和复制赋值运算符,它们仅在复制时调用。如果你真的想彻底,可以考虑五法则,它增加了一个移动构造函数和移动赋值运算符,可以在不复制的情况下窃取资源。移动语义是在 C++11 中引入的