文章目录
- [一、list 的底层结构](#一、list 的底层结构)
- [二、list 和 vector 的最大区别:迭代器设计](#二、list 和 vector 的最大区别:迭代器设计)
- 三、反向迭代器:用正向迭代器包装出来
- 四、常用接口速查
- [五、迭代器失效:list 比 vector 简单](#五、迭代器失效:list 比 vector 简单)
- [六、list 的模拟实现](#六、list 的模拟实现)
-
- [6.1 list 本体](#6.1 list 本体)
- [6.2 为什么哨兵节点让代码更简洁](#6.2 为什么哨兵节点让代码更简洁)
- [七、vector vs list:选择框架](#七、vector vs list:选择框架)
- 总结
vector 胜在连续内存和随机访问,但一遇到频繁的中间插入删除就萎了------搬移元素是 O(n) 的硬伤。list 正是为这个场景设计的:底层是带头结点的双向循环链表,任意位置插入删除都是 O(1)。这篇从底层结构拆起,重点讲迭代器的封装设计和反向迭代器的实现技巧,最后给出 vector vs list 的选型决策框架。
一、list 的底层结构
cpp
template<class T>
struct __list_node
{
__list_node<T>* _prev; // 前驱节点
__list_node<T>* _next; // 后继节点
T _data; // 节点数据
};
每个节点是一个独立的堆对象,节点之间通过指针串联。list 本身只持有一个哨兵节点:
┌───────────────────────────────────────────┐
│ list 对象 │
│ ┌─────┐ │
│ │_head│→ [哨兵] ←→ [节点1] ←→ [节点2] ←→ │
│ └─────┘ ↑___________________________↓ │
└───────────────────────────────────────────┘
哨兵节点的 _next 指向首节点,_prev 指向尾节点;首节点的 _prev 指向哨兵,尾节点的 _next 指向哨兵。环形结构 让边界条件统一------空链表时哨兵的前后都指向自己,不需要任何 nullptr 判断。
二、list 和 vector 的最大区别:迭代器设计
vector 的迭代器是原生指针 T*,直接 ++it 就是偏移一个元素大小。list 不行------list 的节点在内存中是分散的,++it 需要跳到下一个节点的地址。
list 的迭代器必须是一个类,封装了节点的跳转逻辑:
cpp
template<class T>
struct __list_iterator
{
typedef __list_node<T>* NodePtr;
NodePtr _node; // 指向当前节点
// 解引用 → 返回节点数据的引用
T& operator*() { return _node->_data; }
// ++ → 移动到下一个节点
__list_iterator& operator++()
{
_node = _node->_next;
return *this;
}
// -- → 移动到上一个节点
__list_iterator& operator--()
{
_node = _node->_prev;
return *this;
}
bool operator!=(const __list_iterator& it) const
{
return _node != it._node;
}
};
list 迭代器的本质:把「指针」封装成「迭代器对象」 ,让 ++/-- 这些操作被重载为链表节点的前后跳转。对使用者来说 ++it 的语义和 vector 完全一致,但底层实现截然不同------这就是 STL 迭代器抽象的价值。
三、反向迭代器:用正向迭代器包装出来
反向迭代器的核心思路是复用正向迭代器的所有逻辑,只颠倒方向:
正向遍历: begin → ... → end
反向遍历: rbegin = end的前一个位置 → ... → rend = begin的前一个位置
关键设计:rbegin() 返回的是 end() 的前一个位置,rend() 返回的是 begin() 的前一个位置。这样 ++ 反向迭代器就等价于 -- 正向迭代器。
cpp
template<class Iterator>
class ReverseIterator
{
public:
typedef typename Iterator::Ref Ref;
typedef typename Iterator::Ptr Ptr;
ReverseIterator(Iterator it) : _it(it) {}
// 解引用 ------ 注意:返回的是 _it 的前一个位置的值
// 因为 rbegin 指向 end(),实际数据在 end() 前面
Ref operator*()
{
Iterator tmp = _it;
--tmp;
return *tmp;
}
// ++反向迭代器 → --正向迭代器
ReverseIterator& operator++()
{
--_it;
return *this;
}
// --反向迭代器 → ++正向迭代器
ReverseIterator& operator--()
{
++_it;
return *this;
}
bool operator!=(const ReverseIterator& it) const
{
return _it != it._it;
}
private:
Iterator _it; // 包装的正向迭代器
};
解引用返回「前一个位置」的数据,这个设计是反向迭代器里最精妙也最容易写错的地方。想象一下:rbegin() 返回的迭代器内部存的是一个指向哨兵节点的正向迭代器,哨兵没有数据,真正的最后一个数据在哨兵前面的节点里------所以 operator* 必须先 --_it 再返回。
通用性 :这个反向迭代器不依赖任何具体容器------只要求 Iterator 支持 ++、--、*。所以同一个 ReverseIterator 模板可以同时用于 list 和 vector 的反向迭代器,无需重写。
四、常用接口速查
cpp
list<int> l;
// 头尾操作 ------ 全是 O(1)
l.push_back(1);
l.push_front(2);
l.pop_back();
l.pop_front();
// 任意位置插入删除 ------ O(1)
auto it = l.begin();
++it;
l.insert(it, 10); // 在 it 前面插入 10
it = l.erase(it); // 删除 it 指向的节点,返回下一个
// front/back ------ 直接访问首尾元素
int val = l.front();
l.back() = 20;
五、迭代器失效:list 比 vector 简单
cpp
list<int> l = {1, 2, 3, 4, 5, 6};
auto it = l.begin();
while (it != l.end())
{
if (*it % 2 == 0)
it = l.erase(it); // erase 返回下一个有效迭代器
else
++it;
}
list 的迭代器失效规则:
| 操作 | 哪些迭代器失效 |
|---|---|
insert |
不失效------新节点插入链表,已有节点的地址不变 |
erase |
仅被删除的节点失效,其他节点不受影响 |
push_back/push_front |
不失效 |
pop_back/pop_front |
仅被删除的节点失效 |
和 vector 的最大区别:list 的节点地址在整个生命周期中从不改变------没有扩容搬迁这回事。这是链表结构自带的稳定性。
六、list 的模拟实现
6.1 list 本体
cpp
template<class T>
class list
{
typedef __list_node<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator;
list()
{
_head = new Node; // 创建哨兵节点
_head->_next = _head;
_head->_prev = _head; // 空链表:前后都指向自己
}
iterator begin() { return iterator(_head->_next); }
iterator end() { return iterator(_head); } // end 就是哨兵
void push_back(const T& x)
{
insert(end(), x); // 在哨兵前面插入 = 尾部插入
}
void push_front(const T& x)
{
insert(begin(), x); // 在首节点前面插入
}
void pop_back() { erase(--end()); }
void pop_front() { erase(begin()); }
// insert:在 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);
}
// erase:删除 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);
}
~list()
{
clear();
delete _head;
}
void clear()
{
auto it = begin();
while (it != end())
it = erase(it);
}
private:
Node* _head; // 哨兵节点
};
6.2 为什么哨兵节点让代码更简洁
如果没有哨兵节点,空链表时 _head 是 nullptr,每次插入删除都要判断是不是头节点、是不是空链表。有了哨兵节点之后,首节点和中间节点在结构上完全平等------只管插、只管删,空链表时哨兵的前后都指向自己,插入逻辑不需要任何特判。
七、vector vs list:选择框架
| 维度 | vector | list |
|---|---|---|
| 底层 | 连续动态数组 | 双向循环链表(带头节点) |
| 随机访问 | O(1) | O(n) |
| 任意位置插入删除 | O(n)(搬移元素) | O(1)(调指针) |
| 扩容 | 需重新分配+拷贝 | 无需扩容,节点独立 |
| CPU缓存 | 极好(连续内存) | 差(节点分散) |
| 内存开销 | 元素本身 + 少量预留空间 | 元素 + 2个指针(16字节) |
| 迭代器 | T* 原生指针 |
封装节点指针的类对象 |
| 插入时迭代器 | 可能全部失效 | 不失效 |
| 删除时迭代器 | 当前位置及之后失效 | 仅当前位置失效 |
决策树:
- 需要
v[100]这种随机访问 → vector - 频繁在中间做插入删除 → list
- 不确定 → vector(连续内存对CPU缓存的红利远超直觉,大部分场景的插入删除比例不至于让 list 翻身)
- 元素很大且需要频繁搬移 → list 有优势,因为搬指针比搬数据便宜
总结
三条核心认知:
- list 的迭代器不是指针,是类对象 。它封装了链表的跳转逻辑,让
++/--的语义和 vector 保持一致------这是 STL 泛型编程思想的核心体现。 - 反向迭代器是对正向迭代器的镜像封装 。
++反向就是--正向,解引用时取前一个位置。一套代码适用所有支持双向移动的容器。 - 哨兵节点消灭了边界特判。空链表、头插、尾插、中间插入------所有逻辑统一,代码量少一半,bug 也少一半。