我的原始回答缺乏细节。这个回答会给出一个适当的解释,说明如何实现循环链表而不会出现内存泄漏,并且仍然遵守零规则。答案基本上相同,使用一个哨兵,但机制比我最初想象的更加复杂。
关键是使用一个哨兵类型,它的行为就像一个列表节点,但实际上并没有真正共享指向列表头的指针。为了实现这一点,节点类应该分成一个行为对象和一个状态对象。
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));
}
};
那么,这个组织做什么?它通过使用哨兵节点作为断点来避免循环引用的问题。虽然列表的尾部指向哨兵对象,但哨兵对象本身不指向任何东西。相反,它使用列表本身的状态来确定其下一个和前一个邻居。
![List->A->B->C->Sentinel](https://istack.dev59.com/lF0kx.webp)
因此,只有在列表存在时,循环共享指针才能持续存在。一旦列表被销毁,
Item A
就会失去它的引用,通过多米诺效应,
Sentinel
本身也将被销毁。
一个基本点是,哨兵对象本身绝不能直接暴露给列表接口的用户。它应始终保持为列表对象的内部。它本质上代表 STL 类型容器中的
end()
,逻辑上,它永远无法从列表中移除(直到列表本身被销毁)。实际上,这意味着对列表的删除操作需要在传入的迭代器表示哨兵时提前退出。
演示
尝试在线