C++ list 模拟实现(一):手撕双向链表,从节点到迭代器
摘要:上一篇我们讲了 list 迭代器为什么不是指针,很多读者留言说懂了理论但不知道怎么手写。本文将承接上一篇,手把手带你从零实现一个带头双向循环链表。涵盖节点设计、迭代器封装、核心接口实现。文末附完整可运行代码。
一、结构设计:带头双向循环链表
SGI STL 的 list 底层是一个带头节点的双向循环链表。为什么要带头节点?
-
省事:插入和删除操作不用特判"是否在头部"或"是否为空",代码复用率极高。
-
闭环 :头节点的
_next指向第一个真实节点,头节点的_prev指向最后一个真实节点。┌────────────────────────────────────────┐ │ │ ▼ │ [头节点] <──> [节点1] <──> [节点2] <──> [节点3] ─┘(不存数据) 存数据 存数据 存数据
当链表为空时,头节点的 _next 和 _prev 都指向自己。
二、节点类 ListNode 设计
cpp
template <typename T>
struct ListNode {
ListNode* _prev;
ListNode* _next;
T _data;
ListNode(const T& val = T())
: _prev(nullptr), _next(nullptr), _data(val) {}
};
三、迭代器类 ListIterator 封装
这是 list 设计的灵魂。它内部包含一个节点指针,通过重载各种运算符来伪装成一个指针。
cpp
template <typename T, typename Ref, typename Ptr>
struct ListIterator {
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node;
ListIterator(Node* node) : _node(node) {}
// 解引用
Ref operator*() { return _node->_data; }
// 成员访问
Ptr operator->() { return &(_node->_data); }
// 前置++
Self& operator++() {
_node = _node->_next;
return *this;
}
// 后置++
Self operator++(int) {
Self tmp(*this);
_node = _node->_next;
return tmp;
}
// 前置--
Self& operator--() {
_node = _node->_prev;
return *this;
}
// 后置--
Self operator--(int) {
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const Self& it) const { return _node != it._node; }
bool operator==(const Self& it) const { return _node == it._node; }
};
💡 设计亮点 :使用了三个模板参数
T, Ref, Ptr,通过传入T&, T*(普通迭代器)或const T&, const T*(const迭代器)来复用同一套代码。这是 STL 的经典技巧,面试说出这点绝对加分。
四、list 类的核心骨架
cpp
template <typename T>
class list {
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
// 构造函数:创建空的头节点
list() {
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
// 析构函数:清理所有节点
~list() {
clear();
delete _head;
_head = nullptr;
}
iterator begin() { return iterator(_head->_next); }
iterator end() { return iterator(_head); }
const_iterator begin() const { return const_iterator(_head->_next); }
const_iterator end() const { return const_iterator(_head); }
void push_back(const T& x) { insert(end(), x); }
void push_front(const T& x) { insert(begin(), x); }
// 在 pos 位置前插入节点
iterator insert(iterator pos, const T& x) {
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
// 删除 pos 位置的节点
iterator erase(iterator pos) {
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
void clear() {
iterator it = begin();
while (it != end()) {
it = erase(it);
}
}
private:
Node* _head;
};
五、核心测试代码
cpp
void test_list() {
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_front(0);
cout << "遍历: ";
for (auto e : lt) cout << e << " ";
cout << endl; // 期望:0 1 2 3
// 测试 erase 返回值
auto it = lt.begin();
++it;
lt.erase(it); // 删除 1
cout << "删除后: ";
for (auto e : lt) cout << e << " ";
cout << endl; // 期望:0 2 3
}
六、面试高频 FAQ
Q1:list 的 insert 会导致迭代器失效吗?
不会。 list 插入时只改变相邻节点的指针,原有节点的内存地址并没有改变。除了被删除的那个节点,其他迭代器依然有效。
Q2:为什么 list 的迭代器要重载 ++ 和 -- ?
因为 list 的内存是不连续的,原生指针的 ++(地址加 sizeof)无法找到下一个节点。必须通过重载 ++ 来实现 _node = _node->_next。
Q3:为什么 list 的迭代器不支持 it + 3?
因为链表不支持随机访问(Random Access)。找到第 n 个节点必须从头遍历,时间复杂度是 O(n),所以标准库没有实现 operator+。
七、总结与挑战
| 核心点 | 结论 |
|---|---|
| 底层结构 | 带头双向循环链表 |
| 迭代器本质 | 封装了 Node* 的类,重载了运算符 |
| 插入删除 | O(1),不会导致其他迭代器失效 |
| 随机访问 | 不支持,只能遍历 |
核心结论 :list 的模拟实现比 vector 更考验面向对象设计能力。理解了迭代器封装,你就掌握了 STL 迭代器模式的核心。建议亲手敲一遍代码,并尝试实现 pop_back、pop_front 和 size()。
👇 评论区互动 :
你觉得 list 的代码难度比 vector 高还是低?
扣1 觉得 list 简单,扣2 觉得迭代器封装太绕了。
评论区见!
🔥 系列爆款推荐 :
👉 [C++ vector 迭代器失效:一张图+3句话,面试直接背](#C++ vector 迭代器失效:一张图+3句话,面试直接背)
👉 [C++ list 迭代器为什么不是指针?一张图讲透双向链表底层](#C++ list 迭代器为什么不是指针?一张图讲透双向链表底层)