[英]Circular double linked list with smart pointers in c++
是否可以在 C++ 中使用智能指针创建一个循环双向链表
struct Node {
int val;
shared_ptr<Node> next;
weak_ptr prev;
};
shared_ptr<Node> head;
但这将具有共享指针的循环引用,因此无法正确解除分配。
使循环链表本身成为一个类(使用您需要的任何操作来构建它,例如附加)。 通过设置 tail->next = nullptr 使其析构函数断开链接。 您断开哪个链接无关紧要,因此如果您不使用头和尾,只需将其中任何一个设置为 nullptr,就可以了。
在我的测试中,我做了一个循环链表,节点没有破坏。 然后在最后,我在它退出之前添加了 tail->next = nullptr,并且所有的析构函数都正确地触发了。
我最初发布的答案在细节上相当简单。 这个给出了如何在没有内存泄漏的情况下实现循环链表并仍然遵守零规则的正确解释。 答案基本相同,使用哨兵,但机制比我最初提出的要复杂一些。
诀窍是使用一个哨兵类型,它的行为就像一个列表节点,但实际上并没有真正具有指向列表头部的共享指针。 为此,节点类应分为行为对象和状态对象。
class NodeState {
std::shared_ptr<Node> next_;
std::weak_ptr<Node> prev_;
int value_;
NodeState (int v) : value_(v) {}
NodeState (std::shared_ptr<Node> p) : next_(p), prev_(p) {}
//...
};
class Node {
virtual ~Node () = default;
virtual NodeState & state () = 0;
std::shared_ptr<Node> & next () { return state().next_; }
std::weak_ptr<Node> & prev () { return state().prev_; }
int & value () { return state().value_; }
void insert (const std::shared_ptr<Node> &p) {
//...
}
};
现在,您可以定义一个节点实现和一个哨兵实现。
class NodeImplementation : public Node {
NodeState state_;
NodeState & state () { return state_; }
NodeImplementation (int v) : state_(v) {}
//...
};
class NodeSentinel : public Node {
List &list_;
NodeSentinel (List &l) : list_(l) {}
NodeState & state () { return list_.sentinel_state_; }
};
列表本身包含一个由哨兵对象使用的NodeState
。 初始化时,列表创建一个哨兵对象并初始化其状态。
class List {
//...
NodeState sentinel_state_;
std::shared_ptr<Node> head () { return sentinel_state_.next_; }
std::shared_ptr<Node> sentinel () {
return std::shared_ptr<Node>(head()->prev());
}
//...
public:
List () : sentinel_state_(std::make_shared<NodeSentinel>(*this)) {}
//...
void push_front (int value) {
head()->insert(std::make_shared<NodeImplementation>(value));
}
void push_back (int value) {
sentinel()->insert(std::make_shared<NodeImplementation>(value));
}
//...
};
那么,这个组织是做什么的呢? 它通过使用哨兵节点作为中断来避免循环引用的问题。 虽然列表的尾部指向哨兵对象,但哨兵对象本身并不指向任何东西。 相反,它使用列表本身的状态来确定其下一个和前一个邻居。
因此,循环共享指针仅在列表存在时才持续存在。 一旦列表被销毁, Item A
失去其引用,并且通过多米诺骨牌效应, Sentinel
本身将被销毁。
一个基本点是哨兵对象本身绝不能直接暴露给列表界面的用户。 它应该始终保持在列表对象的内部。 它本质上表示类似 STL 的容器中的end()
,并且从逻辑上讲,它永远不能从列表中删除(直到列表本身被销毁)。 实际上,这意味着如果传入的迭代器代表哨兵,则列表上的删除操作需要提前退出。
也可以定义一个成员函数 next() ,它可以在共享指针或弱指针之间进行选择。
#include <iostream>
#include <memory>
using namespace std;
struct T {
int n_;
shared_ptr<T> next_;
weak_ptr<T> weaknext_;
T(shared_ptr<T> next, int n) : next_(next), n_(n) {};
auto next() {
if (next_ == nullptr)
return shared_ptr<T>(weaknext_);
return next_;
}
~T() { cout << n_ << "ok\n"; }
};
int main() {
auto p0 = make_shared<T>(nullptr, 1);
auto p1 = make_shared<T>(p0, 2);
auto p2 = make_shared<T>(p1, 3);
p0->weaknext_ = p2; //makes the list circular
auto p = p2;
for (int i = 0; i < 5; ++i) {
cout << p->n_ << "\n";
p = p->next();
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.