具有完整五法则的简单链表

Simple linked list with full set Rule of Five

本文关键字:简单链表      更新时间:2023-10-16

我正在尝试正确实现一个尊重 5 规则的简单链表。我到了大约 3 点,虽然我已经在这里有了怀疑,但从那里开始,我如履薄冰。由于这似乎是一个相当普遍的话题,我很惊讶我找不到一个完整的例子。我找到了零碎的东西,但没有完整的一套。因此,如果我对此进行排序,它也可以作为未来的参考。

我为一些现实生活中的"复杂性"添加了一个示例class Data,因为大多数示例只有一个int节点和一个指向下一项的指针。

编辑:我已经用PaulMcKenzie下面的代码完成了这个类,它在VS2019中编译正常,但对移动构造函数和赋值运算符发出警告:C26439: This kind of function may not throw. Declare it 'noexcept' (f.6)

class Data
{
public:
int id;
string name;
float[5] datapoints;
};
class Node
{
public:
Node(Data d = { 0 }, Node* n = nullptr) : data(d), next(n) {};
Data& GetData() { return data; }
Node*& GetNext() { return next; }
private:
Data data;
Node* next;
};
class NodeList
{
public:
NodeList() :head(nullptr) {}              // constructor
~NodeList();                              // 1. destructor
NodeList(const NodeList& src);            // 2. copy constructor
NodeList& operator=(const NodeList& src); // 3. copy assignment operator
NodeList(NodeList&& src);                 // 4. move constructor
NodeList& operator=(NodeList&& src);      // 5. move assignment operator
void AddToNodeList(Data data);            // add node
private:
Node* head;
};
void NodeList::AddToNodeList(Data data)
{
head = new Node(data, head);
}
NodeList::~NodeList()
{
Node* n = head, * np;
while (n != nullptr)
{
np = n->GetNext();
delete n;
n = np;
}
}
NodeList::NodeList(const NodeList & src) : head(nullptr)
{
Node* n = src.head;
while (n != nullptr)
{
AddToNodeList(n->GetData());
n = n->GetNext();
}
}
NodeList& NodeList::operator= (const NodeList& src)
{
if (&src != this)
{
NodeList temp(src);
std::swap(head, temp.head);
}
return *this;
}
NodeList::NodeList(NodeList&& src) : head{src.head}
{
src.head = nullptr;
}
NodeList& NodeList::operator=(NodeList&& src)
{
if (this != &src)
std::swap(src.head, head);
return *this;
}

首先要解决的是您的赋值运算符不正确。 您正在使用复制/交换成语,但您忘记进行复制。

NodeList& NodeList::operator=(NodeList src)  
{
std::swap(head, src.head);
return *this;
}

请注意从const NodeList&NodeList src的更改作为参数。 这将使编译器自动为我们执行复制,因为参数是按值传递的。

如果仍希望通过 const 引用传递,则需要进行以下更改:

NodeList& NodeList::operator=(const NodeList& src) 
{
if ( &src != this )
{
NodeList temp(src);  // copy
std::swap(head, temp.head);
}
return *this;
}

请注意自分配的附加测试。 这确实不是必需的,但可能会加快代码速度(但同样,不能保证(。

至于这是否是最有效的方法,还有待商榷——这完全取决于对象。 但有一件事是肯定的——如果你使用复制/交换习惯用法(正确(,就不会有错误、悬空指针或内存泄漏。


现在进入移动功能:

要实现缺少的函数,基本上应该从现有对象中删除内容,并从传入的对象中窃取内容:

首先,移动构造器:

NodeList::NodeList(Nodelist&& src) : head{src.head} 
{
src.head = nullptr;
}

我们真正想做的是从src中窃取指针,然后将src.head设置为nullptr。 请注意,这将使src可破坏,因为src.head将被nullptr(并且您的NodeList析构函数正确处理nullptr(。

现在对于移动分配:

Nodelist& operator=(NodeList&& src) 
{
if ( this != &src )
std::swap(src.head, head);
return *this;
}

我们检查自我分配,因为我们不想从自己那里偷东西。 实际上,我们真的没有偷任何东西,只是把东西换掉了。 然而,与赋值运算符不同的是,没有进行任何复制 - 只是内部的交换(这基本上是之前修复的错误赋值运算符正在做的事情(。 这允许src在调用src析构函数时销毁旧内容。

请注意,在移动(构造或赋值(之后,传入对象基本上处于一种状态,该状态可能会也可能不会使对象可用,或者如果不可用,则稳定(因为传入对象的内部可能已更改(。

调用方仍然可以使用此类对象,但使用可能处于稳定状态或可能不处于稳定状态的对象存在所有风险。 因此,对于调用者来说,最安全的做法是让对象消失(这就是为什么在 move 构造函数中,我们将指针设置为nullptr(。