具有完整五规则集的简单链表
Simple linked list with full set Rule of Five
我正在尝试正确地实现一个遵循 5 规则的简单链表。我得到了大约 3,虽然我在这里已经有疑虑,但从那以后,我如履薄冰。由于这似乎是一个相当普遍的主题,令我惊讶的是我找不到完整的示例。我找到了点点滴滴,但没有完整的集合。所以如果我把它整理好,它也可以作为未来的参考。
我添加了一个示例 class Data
用于一些现实生活 "complexity",因为大多数示例只有一个节点,其中只有一个 int
和一个指向下一项的指针。
编辑:我已经使用 PaulMcKenzie 如下所示的代码完成了 class,它在 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;
}
注意自我分配的附加测试。这确实没有必要,但是 可能 加快代码速度(但同样不能保证)。
至于这是否是最有效的方法,还有待商榷——这完全取决于对象。但有一件事是肯定的——如果您使用 copy/swap 惯用语(正确),就不会有错误、悬挂指针或内存泄漏。
现在开始移动功能:
要实现缺少的功能,基本上应该从现有对象中删除内容,并从传入的对象中窃取内容:
首先,移动构造函数:
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
析构函数时销毁旧内容。
请注意,在移动(构造或赋值)之后,传入的对象基本上处于一种状态,该状态可能会或可能不会使该对象可用,或者如果不可用,则稳定(因为潜在地,传入的对象已更改)。
调用者仍然可以使用这样的对象,但要承担使用可能处于稳定状态或不稳定状态的对象的所有风险。因此对于调用者来说最安全的事情是让对象消亡(这就是为什么在移动构造函数中,我们将指针设置为nullptr
)。
我正在尝试正确地实现一个遵循 5 规则的简单链表。我得到了大约 3,虽然我在这里已经有疑虑,但从那以后,我如履薄冰。由于这似乎是一个相当普遍的主题,令我惊讶的是我找不到完整的示例。我找到了点点滴滴,但没有完整的集合。所以如果我把它整理好,它也可以作为未来的参考。
我添加了一个示例 class Data
用于一些现实生活 "complexity",因为大多数示例只有一个节点,其中只有一个 int
和一个指向下一项的指针。
编辑:我已经使用 PaulMcKenzie 如下所示的代码完成了 class,它在 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;
}
注意自我分配的附加测试。这确实没有必要,但是 可能 加快代码速度(但同样不能保证)。
至于这是否是最有效的方法,还有待商榷——这完全取决于对象。但有一件事是肯定的——如果您使用 copy/swap 惯用语(正确),就不会有错误、悬挂指针或内存泄漏。
现在开始移动功能:
要实现缺少的功能,基本上应该从现有对象中删除内容,并从传入的对象中窃取内容:
首先,移动构造函数:
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
析构函数时销毁旧内容。
请注意,在移动(构造或赋值)之后,传入的对象基本上处于一种状态,该状态可能会或可能不会使该对象可用,或者如果不可用,则稳定(因为潜在地,传入的对象已更改)。
调用者仍然可以使用这样的对象,但要承担使用可能处于稳定状态或不稳定状态的对象的所有风险。因此对于调用者来说最安全的事情是让对象消亡(这就是为什么在移动构造函数中,我们将指针设置为nullptr
)。