链表:如何实现析构函数、复制构造函数和复制赋值运算符?
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++ 上遵循三法则。其中实现:Destructor、Copy Constructor和Copy 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 的规则。 @Kevinkunl1 = l2;
-- 和 -- LinkedList l3 = l1;
-- 你试过那个简单的测试吗?您的main
程序似乎避免了实际测试复制和分配。只需编写这些测试行,您就会看到事情崩溃或正常工作。
一个可能的实现是在你不需要的时候明确delete复制ctor和赋值操作符。这样,如果您的代码稍后尝试使用它们,您将收到编译时错误。当您在merge
中销毁 l2
时,我会使用&&
引用来明确说明。
你必须实现析构函数。您不必实现另外两个,您可以选择将它们设为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 &src)
和LinkedList& LinkedList::operator=(const LinkedList &rhs)
只有当我在我的代码上执行从现有对象复制到新对象时才会执行?喜欢l1 = l2
或LinkedList l2(l1)
?
是的,它们是复制构造函数和复制赋值运算符,它们仅在复制时调用。如果您真的想要彻底,请考虑五法则,它添加了一个移动构造函数和移动赋值运算符,以窃取资源而不制作副本。移动语义是在 C++11 中引入的【参考方案2】:
这段代码中应用了几个有问题的做法,也有一个bug。
首先,错误。当您创建一个列表时,它会new
s 其所有节点并使用指针跟踪它们。当您将一个列表分配给另一个列表时,您实际上是在复制指针值。您现在不仅丢失了分配列表的节点(因为您覆盖了它们)并且出现内存泄漏(因为现在没有指向分配节点的指针),您现在在两个不同的列表上也有相同的指针,指向相同的节点。当列表被销毁时,他们都尝试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++ 等效项,但它是类型安全的。
使用new
和delete
进行手动内存管理很难正确处理(正如您自己发现的那样)。您可能有兴趣使用std::unique_ptr
或std::shared_ptr
来减轻负担。他们会发现这个错误的。
现在,请:不要用 C++ 编写,就像使用带有类的 C 一样。我知道您可能没有遇到我在这里介绍的所有功能,但无论如何现在您都知道它们了:)
【讨论】:
感谢您的可疑做法!我理解您对内存泄漏的解释。但是,哪个创建代码是错误的?你的意思是这个创建代码是错误的:LinkedList l1(arr1, size1);
和 LinkedList l2(arr2, size2);
吗?以及我必须将哪些代码与赋值运算符一起使用?
因为它的代码实际上可以完美运行。我通过按引用传递来处理内存泄漏,并通过分配b.first = NULL
和b.last=NULL
来销毁l2
,因此当调用析构函数时,它不会双重释放。我的问题是,我想知道我的代码是否有使用三法则的好习惯?
对于评论 1:我所说的错误发生在您分配 LinkedList
时。即使您没有定义赋值运算符,编译器也为您定义了一个恰好做错事的运算符。 (注意:您没有在发布的代码中使用赋值运算符。这就是为什么一些 cmets 建议将其标记为 delete
d (与内存释放 btw 完全不同的东西),因此它永远不会被调用。然而,错误仍然存在在那里,它只是没有展示自己)。
对于评论 2:当您不能遵循零规则时,使用三规则通常是一种很好的做法。也就是说,当编译器提供构造函数和赋值运算符时,不会做你想做的事。这里的底线是你写的代码越少越好,所以如果可以的话让编译器为你写。 已编辑: 答案部分错误,因为我以为您在谈论代码的其他部分。
另外,这里有个提示:当你 new
时,你也可以调用 Node
的构造函数。编译器为您生成了一个在这种情况下执行您想要的操作(查找聚合初始化)。以上是关于链表:如何实现析构函数、复制构造函数和复制赋值运算符?的主要内容,如果未能解决你的问题,请参考以下文章