

❤️@燃于AC之乐 来自重庆 计算机专业的一枚大学生
✨专注 C/C++ Linux 数据结构 算法竞赛 AI
🏞️志同道合的人会看见同一片风景!
👇点击进入作者专栏:
🌟《算法画解》算法相关题目点击即可进入实操🌟
感兴趣的可以先收藏起来,请多多支持,还有大家有相关问题都可以给我留言咨询,希望希望共同交流心得,一起进步,你我陪伴,学习路上不孤单!
文章目录
- 前言
- [一、STL list概述与设计哲学](#一、STL list概述与设计哲学)
-
- [1.1 vector与list的对比](#1.1 vector与list的对比)
- [1.2 何时使用list?](#1.2 何时使用list?)
- 二、list节点结构设计
-
- [2.1 STL源码中的节点定义](#2.1 STL源码中的节点定义)
- [2.2 手写实现的节点结构](#2.2 手写实现的节点结构)
- 三、list迭代器设计精髓
-
- [3.1 为什么list需要特殊迭代器?](#3.1 为什么list需要特殊迭代器?)
- [3.2 STL迭代器设计](#3.2 STL迭代器设计)
- [3.3 手写迭代器实现](#3.3 手写迭代器实现)
- 四、核心操作源码剖析
-
- [4.1 transfer操作:list的灵魂](#4.1 transfer操作:list的灵魂)
- [4.2 基于transfer的splice操作](#4.2 基于transfer的splice操作)
- [4.3 手写实现的插入删除操作](#4.3 手写实现的插入删除操作)
- 五、list的高级操作
-
- [5.1 merge合并操作](#5.1 merge合并操作)
- [5.2 reverse反转操作](#5.2 reverse反转操作)
- [5.3 sort排序操作](#5.3 sort排序操作)
- 六、完整手写list实现
-
- [6.1 list类框架](#6.1 list类框架)
- [6.2 初始化与清理](#6.2 初始化与清理)
- [6.3 常用接口实现](#6.3 常用接口实现)
- 七、测试与验证
-
- [7.1 基础功能测试](#7.1 基础功能测试)
- [7.2 迭代器失效测试](#7.2 迭代器失效测试)
- [7.3 复杂类型支持](#7.3 复杂类型支持)
- 八、关键特性总结
-
- [8.1 list的核心优势](#8.1 list的核心优势)
- [8.2 与vector的对比选择](#8.2 与vector的对比选择)
- [8.3 手写实现的收获](#8.3 手写实现的收获)
- 九、学习建议
前言
STL(Standard Template Library)作为C++标准库的核心组件,其精妙的设计思想和高效的实现一直是C++开发者深入学习的宝藏。在众多容器中,list双向链表以其独特的节点式存储结构和稳定的迭代器特性,展现了与vector截然不同的设计哲学。
本文将从STL源码的深度剖析出发,结合亲手实现的简化版list,带领大家一探双向链表的设计精髓。我们将从节点结构、迭代器设计、核心操作实现等多个维度,层层揭开STL list的神秘面纱,理解其为何能在插入删除操作中保持高效,以及迭代器稳定性的实现机制。
无论你是正在学习数据结构,还是希望深入理解STL内部机制,相信通过本文的源码级分析和实践指导,都能获得新的启发和收获。
深入理解STL双向链表的设计精髓,掌握list容器的底层实现原理
一、STL list概述与设计哲学
1.1 vector与list的对比
STL中的list是双向链表容器,与vector的连续线性空间形成鲜明对比:
| 特性 | vector | list |
|---|---|---|
| 存储方式 | 连续内存空间 | 分散节点存储 |
| 插入/删除 | 可能引起内存重分配 | O(1)时间复杂度 |
| 随机访问 | O(1) | O(n) |
| 空间使用 | 可能浪费 | 精准配置 |
核心差异 :list每次插入或删除一个元素,就配置或释放一个元素空间,对空间的运用有绝对的精简。
1.2 何时使用list?
根据《STL源码剖析》的建议,选择容器应考虑:
- 元素类型和构造复杂度
- 是否频繁在中间位置插入删除
- 是否需要随机访问
- 内存使用效率要求
二、list节点结构设计
2.1 STL源码中的节点定义

STL采用分离设计:list本身和list节点是不同的结构。
cpp
// STL源码中的list节点结构
template <class T>
struct __list_node {
typedef void* void_pointer;
void_pointer prev; // 实际应为__list_node<T>*
void_pointer next;
T data;
};
这是一个典型的双向链表节点,包含前后指针和数据域。
2.2 手写实现的节点结构
cpp
// 手写实现的list节点
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node(const T& data = T())
:_data(data)
,_next(nullptr)
,_prev(nullptr)
{}
};
对比分析:
- STL使用
void_pointer增强通用性 - 手写实现直接使用具体指针类型
- 都包含前驱、后继和数据三个部分
三、list迭代器设计精髓
3.1 为什么list需要特殊迭代器?

list不再能够像vector一样以普通指针作为迭代器,因为:
- 节点不连续:不能通过简单指针运算访问相邻元素
- 需要特殊操作:递增指向下一个节点,递减指向上一个节点
- 需要取值语义:解引用应获取节点数据
3.2 STL迭代器设计
cpp
template<class T, class Ref, class Ptr>
struct _list_iterator {
typedef _list_iterator<T, T&, Ptr> iterator;
typedef bidirectional_iterator_tag iter_type; // 双向迭代器标签
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
// ... 其他成员定义
};
重要性质:
- 插入和接合操作不会造成迭代器失效
- 只有指向被删除元素的迭代器会失效
- 提供Bidirectional Iterators能力
3.3 手写迭代器实现
cpp
template<class T, class Ref, class Ptr>
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T, Ref, Ptr> Self;
Node* _node;
list_iterator(Node* node)
:_node(node)
{}
// 解引用运算符
Ref operator*()
{
return _node->_data;
}
// 箭头运算符(特殊处理)
Ptr operator->()
{
return &_node->_data;
}
// 前置++
Self& operator++()
{
_node = _node->_next;
return *this;
}
// 前置--
Self& operator--()
{
_node = _node->_prev;
return *this;
}
// 后置++
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
// 相等性判断
bool operator!=(const Self& s) const
{
return _node != s._node;
}
};
关键点:
- 迭代器本质是节点的包装器
- 通过运算符重载模拟指针行为
- 箭头运算符的简化处理(省略一个
->)
四、核心操作源码剖析

4.1 transfer操作:list的灵魂
transfer()是list内部最核心的操作,所有复杂操作都基于它:
cpp
// STL源码中的transfer实现
void transfer(iterator position, iterator first, iterator last) {
if (position != last) {
(*link_type)((*last.node).prev)).next = position.node; // (1)
(*link_type)((*first.node).prev)).next = last.node; // (2)
link_type tmp = link_type((*position.node).prev); // (3)
(*position.node).prev = (*last.node).prev; // (4)
(*last.node).prev = (*first.node).prev; // (5)
(*first.node).prev = tmp; // (6)
}
}
作用 :将[first, last)区间内的元素移动到position之前。
4.2 基于transfer的splice操作

splice是transfer的公开接口,用于将元素从一个list移动到另一个:
cpp
// 1. 将整个list x接合到position之前
void splice(iterator position, list& x) {
if (!x.empty()) {
transfer(position, x.begin(), x.end());
}
}
// 2. 将单个元素接合到position之前
void splice(iterator position, list&, iterator i) {
iterator j = i;
++j;
if (position == i || position == j) return;
transfer(position, i, j);
}
// 3. 将区间接合到position之前
void splice(iterator position, list&, iterator first, iterator last) {
if (first != last)
transfer(position, first, last);
}
4.3 手写实现的插入删除操作

cpp
// 插入操作
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
// 调整指针:prev <-> newnode <-> cur
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
++_size;
return newnode; // 返回新插入节点的迭代器
}
// 删除操作(注意迭代器失效问题)
iterator erase(iterator pos)
{
assert(pos != end()); // 不能删除哨兵节点
Node* prev = pos._node->_prev;
Node* next = pos._node->_next;
prev->_next = next;
next->_prev = prev;
delete pos._node;
--_size;
return next; // 返回被删除节点的下一个节点
}
重要特性:
insert后迭代器不失效erase后当前迭代器失效,但返回下一个有效迭代器- 需要维护
_size计数器
五、list的高级操作
5.1 merge合并操作
cpp
template <class T, class Alloc>
void list<T, Alloc>::merge(list<T, Alloc>& x) {
iterator first1 = begin();
iterator last1 = end();
iterator first2 = x.begin();
iterator last2 = x.end();
// 前提:两个lists都已经递增排序
while (first1 != last1 && first2 != last2) {
if (*first2 < *first1) {
iterator next = first2;
transfer(first1, first2, ++next);
first2 = next;
}
else
++first1;
}
if (first2 != last2) transfer(last1, first2, last2);
}
5.2 reverse反转操作
基于transfer的反转实现,通过逐个移动节点实现整个链表的反转。
5.3 sort排序操作
list的sort通常实现为归并排序,因为链表结构适合归并操作。
六、完整手写list实现
6.1 list类框架
cpp
template<class T>
class list
{
typedef list_node<T> Node;
public:
// 迭代器类型定义
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
// 构造函数
list() { empty_init(); }
// 拷贝构造
list(const list<T>& lt) { /*...*/ }
// 赋值运算符
list<T>& operator=(list<T> lt) { swap(lt); return *this; }
// 析构函数
~list() { clear(); delete _head; }
private:
Node* _head; // 哨兵节点
size_t _size; // 元素个数
};
6.2 初始化与清理
cpp
// 空链表初始化(创建哨兵节点)
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
// 清空链表(保留哨兵节点)
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
6.3 常用接口实现
cpp
// 迭代器相关
iterator begin() { return _head->_next; }
iterator end() { return _head; }
// 容量相关
size_t size() const { return _size; }
bool empty() const { return _size == 0; }
// 元素访问
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()); }
七、测试与验证
7.1 基础功能测试
cpp
void test_list1()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
// 迭代器遍历
list<int>::iterator it = lt.begin();
while (it != lt.end()) {
*it += 10; // 修改元素
cout << *it << " ";
++it;
}
cout << endl;
// 范围for遍历
for (auto e : lt) {
cout << e << " ";
}
cout << endl;
}
7.2 迭代器失效测试
cpp
void test_list2()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
// insert后迭代器不失效
list<int>::iterator it = lt.begin();
lt.insert(it, 10);
*it += 100; // 仍然有效
// erase后迭代器失效,需要接收返回值
it = lt.begin();
while (it != lt.end()) {
if (*it % 2 == 0) {
it = lt.erase(it); // 正确写法
} else {
++it;
}
}
}
7.3 复杂类型支持
cpp
struct AA
{
int _a1 = 1;
int _a2 = 2;
};
void test_complex_type()
{
list<AA> lta;
lta.push_back(AA());
list<AA>::iterator ita = lta.begin();
// 箭头运算符简化:ita->_a1 等价于 ita.operator->()->_a1
cout << ita->_a1 << ":" << ita->_a2 << endl;
}
八、关键特性总结
8.1 list的核心优势
- 插入删除高效:任意位置O(1)复杂度
- 空间利用精准:按需分配,无内存浪费
- 迭代器稳定:插入操作不会使迭代器失效
- 支持双向遍历:Bidirectional Iterators
8.2 与vector的对比选择
| 场景 | 推荐容器 | 理由 |
|---|---|---|
| 频繁随机访问 | vector | 连续内存,缓存友好 |
| 频繁中间插入删除 | list | 无需移动元素 |
| 内存紧张 | list | 精准分配 |
| 需要排序 | 看情况 | list排序慢但稳定 |
8.3 手写实现的收获
通过手写list实现,我们深刻理解了:
- 哨兵节点的巧妙设计
- 迭代器与容器的分离
- 异常安全的重要性
- 模板编程的实际应用
九、学习建议
- 先理解原理,再阅读源码:STL源码为了效率做了很多优化,直接阅读可能困难
- 动手实现一遍:只有自己实现过,才能真正理解设计精妙之处
- 关注迭代器失效规则:这是使用STL容器最容易出错的地方
- 理解空间与时间的权衡:不同容器适合不同场景,没有绝对的好坏
参考资料:
- 《STL源码剖析》- 侯捷
- 《Effective STL》- Scott Meyers
- C++标准文档
通过深入剖析STL list的源码和手写实现,我们不仅掌握了双向链表的数据结构,更理解了STL设计哲学:效率、通用性、安全性的完美平衡。希望这篇文章能帮助你更好地理解和使用STL list容器!

加油!志同道合的人会看到同一片风景。
看到这里请点个赞 ,关注 ,如果觉得有用就收藏一下吧。后续还会持续更新的。 创作不易,还请多多支持!