本文系统讲解 C++ STL 中
std::list的全部核心知识:从基础 API 到底层链表结构,再到手写模拟实现。读完这篇文章,list 将不再是你 C++ 学习路上的拦路虎。
一、前言
在 C++ 的 STL 中,vector 统一了"动态数组"的天下,但它并非万能。当我们需要频繁在容器中间位置插入或删除元素时,vector 每次都要移动大量元素,O(n) 的时间复杂度让人头疼。
这时,std::list 应运而生。
list 是 STL 中的双向链表 容器,它在底层维护了一个带头双向循环链表 ,其最大的特点就是:任意位置的插入和删除都是 O(1)。
理解 list,不仅能让你在合适的场景下做出正确的容器选择,还能帮你深入掌握链表这种经典数据结构在工业级代码中的实际运用。
本文特点:
- 分层讲解:从基础用法到底层原理,你可以按需跳读
- 大量可运行代码:每一个接口都配有完整的示例代码
- 模拟实现 :基于课堂完整源码,手写
list容器,包含节点设计、迭代器封装、核心操作、三个测试用例 - 对比学习 :贯穿始终的
list vs vector对比,帮你在实际开发中选对容器
在学习之前,建议你先收藏本文,方便后续参照。
二、list 是什么?
2.1 官方定义
std::list 是 C++ 标准库中一个顺序容器 ,它在内部维护了一个带头双向循环链表。
cpp
template<
class T,
class Allocator = std::allocator<T>
> class list;
💡 注意区分:C++11 之后,标准库还有一个
std::forward_list(单链表),而list特指双向链表。
2.2 核心特性
| 特性 | 说明 |
|---|---|
| 双向链表 | 每个节点包含指向前驱和后继的指针 |
| 插入/删除 O(1) | 只要找到位置,插入和删除都是常数时间 |
| 不支持随机访问 | 不提供 operator[],访问第 N 个元素需遍历 O(n) |
| 迭代器类型 | 双向迭代器(Bidirectional Iterator) |
| 额外开销 | 每个节点多两个指针,内存开销比 vector 大 |
2.3 链表结构图解
list 底层是带头双向循环链表,结构如下:
哨兵位头节点
head
|
v
+----+----+----+
nullptr <-- |prev| |next| --> ...
+----+----+----+
^ |
| v
+----+----+----+ +----+----+----+
|prev| 1 |next| <->|prev| 2 |next|
+----+----+----+ +----+----+----+
- 带头:有一个哨兵位头节点(_head),它不存储有效数据
- 双向 :每个节点有
_prev和_next两个指针 - 循环 :头节点的
_prev指向最后一个节点,最后一个节点的_next指向头节点
这种设计的巧妙之处:
- 头节点的
_next就是第一个有效元素(begin()) - 头节点本身就是
end()(end()指向头节点) - 空链表时:
head._next = head._prev = &head(自己指向自己)
2.4 list vs vector 直观对比
cpp
#include <list>
#include <vector>
int main() {
// vector:连续内存,随机访问快
vector<int> vec = {1, 2, 3, 4, 5};
int x = vec[3]; // O(1),直接拿到 4
vec.insert(vec.begin() + 2, 99); // O(n),要后移元素
// list:链式内存,插入删除快
list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
++it; ++it; // 必须一步步走
lst.insert(it, 99); // O(1),只改指针
// lst = {1, 2, 99, 3, 4, 5}
}
一句话总结:list 是"插入删除快、查找慢"的容器,与 vector 刚好互补。
三、list 的构造方式
3.1 默认构造
cpp
list<int> l1; // 空 list
list<string> l2; // 存储 string 的空 list
3.2 填充构造
cpp
list<int> l3(5); // 5个元素,值均为 int() = 0
list<int> l4(5, 42); // 5个元素,值均为 42
list<string> l5(3, "hello"); // 3个元素,值均为 "hello"
3.3 拷贝构造
cpp
list<int> l6(5, 10); // {10, 10, 10, 10, 10}
list<int> l7(l6); // 深拷贝,l7 = {10, 10, 10, 10, 10}
list<int> l8 = l6; // 等价于拷贝构造
3.4 迭代器范围构造
cpp
int arr[] = {1, 2, 3, 4, 5};
list<int> l9(arr, arr + 5); // {1, 2, 3, 4, 5}
vector<int> v = {10, 20, 30};
list<int> l10(v.begin(), v.end()); // {10, 20, 30}
3.5 初始化列表构造(C++11)
cpp
list<int> l11 = {1, 2, 3, 4, 5};
list<string> l12{"apple", "banana", "cherry"};
3.6 移动构造(C++11)
cpp
list<int> createList() {
list<int> temp = {1, 2, 3, 4, 5};
return temp; // 移动语义,高效
}
list<int> l13 = createList(); // 直接"窃取"资源
四、list 的迭代器
4.1 迭代器类型对比
| 容器 | 迭代器类型 | 支持的操作 |
|---|---|---|
vector |
随机访问迭代器 | ++, --, +n, -n, [], <, > |
list |
双向迭代器 | ++, --(不支持 +n、[]) |
forward_list |
正向迭代器 | 仅 ++(不支持 --) |
4.2 迭代器类型一览
cpp
list<int> lst = {10, 20, 30, 40, 50};
// 正向迭代器
list<int>::iterator it = lst.begin();
list<int>::const_iterator cit = lst.cbegin();
// 反向迭代器
list<int>::reverse_iterator rit = lst.rbegin();
list<int>::const_reverse_iterator crit = lst.crbegin();
4.3 const_iterator vs const iterator ------ 一个重要区分
课堂上很多同学会混淆这两个概念。老师的注释一针见血:
cpp
// 迭代器指向的内容不能修改!const iterator 不是我们需要的 const 迭代器
// T* const p1 ------ 迭代器本身不能改(等价于 const iterator)
// const T* p2 ------ 迭代器指向的内容不能改(等价于 const_iterator)
const iterator:迭代器本身是 const(不能++it),但指向的内容可以改 ------ 实际中几乎不用const_iterator:迭代器指向的内容不能改,但迭代器本身可以++it------ 函数传参时常用,体现只读语义
后面的模拟实现中,你会看到如何通过三个模板参数 (T、Ref、Ptr)来优雅地同时支持 iterator 和 const_iterator,这是课堂实现的核心技巧。
4.4 遍历 list 的三种方式
cpp
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
int main() {
list<int> lst = {10, 20, 30, 40, 50};
// 方式1:迭代器
cout << "迭代器遍历:";
for (list<int>::iterator it = lst.begin(); it != lst.end(); ++it) {
cout << *it << " ";
}
cout << endl;
// 方式2:范围 for(C++11,最推荐)
cout << "范围for遍历:";
for (int x : lst) {
cout << x << " ";
}
cout << endl;
// 方式3:for_each + lambda
cout << "for_each遍历:";
for_each(lst.begin(), lst.end(), [](int x) {
cout << x << " ";
});
cout << endl;
return 0;
}
输出:
迭代器遍历:10 20 30 40 50
范围for遍历:10 20 30 40 50
for_each遍历:10 20 30 40 50
4.4 list 迭代器不能做的操作
cpp
list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
// ✅ 可以
++it; // 指向下一个节点
--it; // 指向前一个节点
it++; // 支持(但建议前置++)
// ❌ 不可以
// it + 2; // 编译错误!不支持随机访问
// it += 3; // 编译错误!
// it < lst.end(); // 编译错误!不支持比较大小
// it[2]; // 编译错误!不支持下标访问
4.5 operator->() 与自定义类型遍历
当 list 存储的是自定义类型(如结构体)时,-> 重载的作用就体现出来了:
cpp
#include <iostream>
#include <list>
using namespace std;
struct A {
int _a1;
int _a2;
A(int a1 = 0, int a2 = 0) : _a1(a1), _a2(a2) {}
};
int main() {
list<A> lt;
lt.push_back(A(1, 1));
lt.push_back({2, 2}); // 初始化列表语法
lt.push_back({3, 3});
list<A>::iterator it = lt.begin();
while (it != lt.end()) {
// 写法1:先解引用再取成员
// cout << (*it)._a1 << ":" << (*it)._a2 << endl;
// 写法2:直接用 ->(推荐,更直观)
cout << it->_a1 << ":" << it->_a2 << endl;
// 写法3:→ 的实际等价调用
// cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << endl;
++it;
}
return 0;
}
输出:
1:1
2:2
3:3
这里的 it->_a1 等价于 it.operator->()->_a1,先调用迭代器的 operator->() 返回 A*,再通过原生指针访问成员。这个机制在后面模拟实现中会详细展开。
4.6 反向遍历
cpp
list<int> lst = {1, 2, 3, 4, 5};
cout << "反向遍历:";
for (auto it = lst.rbegin(); it != lst.rend(); ++it) {
cout << *it << " "; // 5 4 3 2 1
}
cout << endl;
五、list 的容量与元素访问
list 的容量接口比较简洁,因为它不需要像 vector 那样管理预留空间。
5.1 容量接口
cpp
#include <iostream>
#include <list>
using namespace std;
int main() {
list<int> lst = {1, 2, 3};
cout << "size: " << lst.size() << endl; // 3
cout << "empty: " << lst.empty() << endl; // 0(false)
lst.clear();
cout << "after clear, size: " << lst.size() << endl; // 0
cout << "empty: " << lst.empty() << endl; // 1(true)
return 0;
}
注意 :早期的 C++98 标准中,list::size() 的时间复杂度可能是 O(n),不过自 C++11 起,标准要求 size() 必须为 O(1)。
5.2 元素访问
cpp
list<int> lst = {10, 20, 30, 40, 50};
lst.front(); // 10 ------ 首元素引用
lst.back(); // 50 ------ 尾元素引用
为什么 list 没有 operator[] 和 at()?
因为链表不是连续存储,无法通过下标直接计算出地址。要访问第 N 个元素,必须从头节点一个个遍历过去------这完全违背了 operator[] 应当 O(1) 的语义。如果你需要随机访问,应该用 vector 或 deque。
六、list 的增删改操作
这是 list 最强大的部分------任意位置的插入和删除都是 O(1)。
6.1 头部/尾部操作
cpp
#include <iostream>
#include <list>
using namespace std;
int main() {
list<int> lst;
lst.push_back(30); // {30}
lst.push_front(10); // {10, 30}
lst.push_back(40); // {10, 30, 40}
lst.insert(++lst.begin(), 20); // {10, 20, 30, 40}
cout << "list: ";
for (int x : lst) cout << x << " "; // 10 20 30 40
cout << endl;
lst.pop_front(); // {20, 30, 40}
lst.pop_back(); // {20, 30}
cout << "after pop: ";
for (int x : lst) cout << x << " "; // 20 30
cout << endl;
return 0;
}
6.2 insert ------ 任意位置插入
cpp
list<int> lst = {1, 2, 3, 4, 5};
// 在指定位置插入一个元素
auto it = lst.begin();
++it; ++it; // 指向 3(必须一步步走)
lst.insert(it, 99);
// lst = {1, 2, 99, 3, 4, 5}
// 在指定位置插入 n 个相同元素
it = lst.begin();
lst.insert(it, 3, 0);
// lst = {0, 0, 0, 1, 2, 99, 3, 4, 5}
// 在指定位置插入一段迭代器范围
vector<int> v = {100, 200};
lst.insert(lst.end(), v.begin(), v.end());
// lst = {0, 0, 0, 1, 2, 99, 3, 4, 5, 100, 200}
与 vector 的关键区别:list 的 insert 是 O(1),因为只需要修改几个指针,不需要移动任何元素。
6.3 erase ------ 任意位置删除
cpp
list<int> lst = {10, 20, 30, 40, 50};
// 删除单个元素
auto it = lst.begin();
++it; // 指向 20
it = lst.erase(it); // 删除 20,返回下一个有效迭代器
// lst = {10, 30, 40, 50}
// it 指向 30
// 删除一段范围 [first, last)
auto first = lst.begin();
++first; // 指向 30
auto last = lst.end();
--last; // 指向 50
lst.erase(first, last); // 删除 30 和 40
// lst = {10, 50}
list 的 erase 同样是 O(1) 的(删除单个元素时)。
6.4 clear() 与 swap()
cpp
list<int> lst = {1, 2, 3, 4, 5};
lst.clear(); // 清空所有元素,size = 0
list<int> lst2 = {100, 200};
lst.swap(lst2); // 交换两个 list 的内容(O(1))
// lst = {100, 200}
七、list 的独有进阶操作
这是 list 最精彩的部分------STL 为链表量身定制了一些特殊的成员函数,高效且实用。
7.1 splice() ------ 粘接/转移节点
splice 是 list 的"杀手锏"操作:它可以将一个 list 中的节点直接转移到另一个 list ,不需要拷贝数据,只需要修改指针,时间复杂度 O(1)(转移单个节点时)。
cpp
#include <iostream>
#include <list>
using namespace std;
int main() {
list<int> l1 = {1, 2, 3, 4, 5};
list<int> l2 = {10, 20, 30};
// 把 l2 的所有元素移动到 l1 的头部
l1.splice(l1.begin(), l2);
// l1 = {10, 20, 30, 1, 2, 3, 4, 5}
// l2 为空
// 重新填充 l2
l2 = {100, 200, 300};
// 把 l2 的第2个元素移动到 l1 的尾部
auto it = l2.begin();
++it; // 指向 200
l1.splice(l1.end(), l2, it);
// l1 = {10, 20, 30, 1, 2, 3, 4, 5, 200}
// l2 = {100, 300}
return 0;
}
splice 的三种形式:
| 形式 | 说明 | 复杂度 |
|---|---|---|
splice(pos, other) |
将 other 的所有节点转移到 pos 前 |
O(1) |
splice(pos, other, it) |
将 other 中 it 指向的节点转移到 pos 前 |
O(1) |
splice(pos, other, first, last) |
将 other 中 [first, last) 转移到 pos 前 |
O(m)(m 为转移节点数) |
重要特性 :被 splice 的节点,其迭代器不会失效,只是所属容器改变了。
7.2 remove() / remove_if() ------ 条件删除
cpp
list<int> lst = {1, 2, 3, 2, 4, 2, 5};
lst.remove(2); // 删除所有值为 2 的元素
// lst = {1, 3, 4, 5}
lst.remove_if([](int x) { return x % 2 == 0; });
// lst = {3, 5}
注意 :list::remove 和 std::remove 算法不同------list::remove 是真正的删除(erase 掉节点),而 std::remove 只是把元素移动到末尾(需要配合 erase)。
7.3 unique() ------ 去重
cpp
list<int> lst = {1, 1, 2, 3, 3, 3, 4, 5, 5, 5, 5};
lst.unique(); // 删除连续相等的重复元素(只保留一个)
// lst = {1, 2, 3, 4, 5}
// 自定义比较器的去重
list<int> lst2 = {10, 11, 20, 21, 30, 31};
lst2.unique([](int a, int b) { return a / 10 == b / 10; });
// lst2 = {10, 20, 30} // 十位数相同的只保留第一个
⚠️ 注意 :unique() 只能删除连续相等的重复元素。如果元素不是有序的,去重可能不彻底:
cpp
list<int> lst = {1, 2, 1, 2, 1, 2};
lst.unique(); // 没有连续相等的,不变!
// lst = {1, 2, 1, 2, 1, 2}
// 需要先 sort 再 unique
lst.sort();
lst.unique();
// lst = {1, 2}
7.4 sort() ------ list 自带的排序
list 不支持 std::sort,因为 std::sort 要求随机访问迭代器,而 list 的迭代器是双向的。
list 有自己的 sort() 成员函数,底层采用归并排序:
cpp
list<int> lst = {5, 3, 1, 4, 2};
lst.sort(); // 升序排序
// lst = {1, 2, 3, 4, 5}
lst.sort(greater<int>()); // 降序排序
// lst = {5, 4, 3, 2, 1}
list::sort vs std::sort 效率对比:
std::sort (用于 vector) |
list::sort |
|
|---|---|---|
| 迭代器要求 | 随机访问迭代器 | 双向迭代器 |
| 算法 | 内省排序(快排变种) | 归并排序 |
| 时间复杂度 | O(n log n) | O(n log n) |
| 空间复杂度 | O(log n) | O(n)(需要额外节点指针数组) |
实际建议 :如果你需要频繁排序,将 list 的数据拷贝到 vector,用 std::sort 排序,再拷回来------对于较大规模的 list,这样做可能更快。
7.5 merge() ------ 合并有序 list
cpp
list<int> l1 = {1, 3, 5, 7};
list<int> l2 = {2, 4, 6, 8};
l1.merge(l2); // 将 l2 合并到 l1(要求两者都已排序)
// l1 = {1, 2, 3, 4, 5, 6, 7, 8}
// l2 为空(节点被转移走!)
要求 :两个 list 必须有序 (升序),合并后 l2 变为空。
7.6 reverse() ------ 反转链表
cpp
list<int> lst = {1, 2, 3, 4, 5};
lst.reverse();
// lst = {5, 4, 3, 2, 1}
reverse 只需调整指针方向,不需要移动数据,时间复杂度 O(n)。
八、迭代器失效问题
8.1 list 迭代器失效的特点
与 vector 相比,list 的迭代器失效规则宽松很多:
| 操作 | list | vector |
|---|---|---|
insert |
不失效(只改指针,不搬迁数据) | 可能全部失效(扩容) |
erase |
只有被删节点的迭代器失效 | 删除点及之后全部失效 |
push_back |
不失效 | 可能全部失效(扩容) |
push_front |
不失效 | 不支持 |
splice |
不失效(节点被转移) | 不支持 |
sort |
不失效 | N/A |
这是链表的天生优势------只要节点本身还在内存中,指向它的迭代器就有效。
8.2 失效场景:erase
cpp
list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
++it; // it 指向 2
lst.erase(it); // 删除 2
// cout << *it << endl; // 危险!it 指向的节点已被销毁
8.3 安全删除写法
cpp
// 安全删除偶数元素的写法
list<int> lst = {1, 2, 3, 4, 5};
for (auto it = lst.begin(); it != lst.end(); ) {
if (*it % 2 == 0) {
it = lst.erase(it); // erase 返回下一个有效节点
} else {
++it;
}
}
// lst = {1, 3, 5}
与 vector 的区别:list 的 erase 只让被删节点失效,其后的迭代器不失效。因此实际上也可以这样写:
cpp
// 对 list 来说这也是安全的(但对 vector 不安全!)
for (auto it = lst.begin(); it != lst.end(); ++it) {
if (*it % 2 == 0) {
it = lst.erase(it); // erase 返回下一个节点
// 注意:for 里的 ++it 可能导致跳跃一个元素
}
}
但为了代码的通用性(以及避免混淆),建议统一使用 it = erase(it) 的写法。
8.4 splice 不导致迭代器失效
cpp
list<int> l1 = {1, 2, 3};
list<int> l2 = {10, 20, 30};
auto it = l2.begin(); // it 指向 10
l1.splice(l1.end(), l2, it); // 把 10 转移到 l1
cout << *it << endl; // ✅ 安全!it 仍然有效,输出 10
// 只是 it 现在属于 l1 了
九、list 的底层结构深入
9.1 节点结构
list 的底层是一个带头双向循环链表。我们先来看节点的设计:
cpp
template<typename T>
struct ListNode {
ListNode<T>* _prev; // 指向前驱节点
ListNode<T>* _next; // 指向后继节点
T _data; // 存储的数据
};
9.2 带头双向循环链表的优势
"带头双向循环"这四个字分别代表:
| 特性 | 解释 | 好处 |
|---|---|---|
| 带头 | 有一个哨兵位头节点,不存数据 | end() 指向头节点,统一空/非空处理 |
| 双向 | 每个节点有 _prev 和 _next |
支持反向遍历,erase 时方便找到前驱 |
| 循环 | 头节点 _prev 指向尾节点,尾节点 _next 指向头节点 |
无需特殊判断边界情况 |
9.3 哨兵位头节点的作用
空链表时:head._next = head._prev = &head
head
/ ^ \
\_/ \_
非空链表时:head._next = 第一个节点,head._prev = 最后一个节点
head <-> node1 <-> node2 <-> ... <-> nodeN
^ |
|_______________________________________|
哨兵头的存在使得:即使链表为空,begin() 和 end() 也能返回合法的迭代器,不需要单独处理空链表的情况。
9.4 list 的内存布局
每个节点在堆上独立分配:
节点1: [prev][data][next]
节点2: [prev][data][next]
节点3: [prev][data][next]
...
节点在内存中**不一定连续**(与 vector 不同),
这也是 list 不支持随机访问的根本原因。
空间开销 :每个节点除了存储 T 类型的数据外,还需要额外的两个指针。对于 int 这种小类型,指针开销可能超过数据本身。
十、list 的模拟实现(基于课堂代码,附完整源码)
理解 list 的最好方式就是自己写一个完整的模拟实现。下面我们对齐课堂老师给出的完整实现代码,深入剖析每一个设计选择。
💡 课堂约定 :老师将所有代码放在
namespace bit中,这是教学代码的常见做法,避免与标准库冲突。我们以下的分析也保持这一命名。
10.1 节点设计
cpp
template<typename T>
struct ListNode {
ListNode<T>* _prev;
ListNode<T>* _next;
T _data;
ListNode(const T& val = T())
: _prev(nullptr), _next(nullptr), _data(val) {}
};
10.2 迭代器封装(进化史:从两个类到一个模板)
这是 list 模拟实现的灵魂所在,也是面试高频考点。课堂代码清晰地展示了设计的进化过程。
阶段一:分别写两个类(已注释,但展示了约束过程)
老师先注释掉了专门为 const 编写的独立迭代器类:
cpp
// 单独为 const 写一个迭代器类(代码冗余,已废弃)
template<class T>
struct ListConstIterator {
typedef ListNode<T> Node;
Node* _node;
ListConstIterator(Node* node) : _node(node) {}
const T& operator*() { return _node->_data; }
const T* operator->() { return &_node->_data; }
// ++、--、!=、== 与普通迭代器代码完全一样
};
问题 :除了 operator* 和 operator-> 的返回值类型不同(T& vs const T&),其余代码完全重复。
阶段二:三模板参数复用(最终方案)
用一个模板参数 Ref 控制 operator* 的返回类型,Ptr 控制 operator-> 的返回类型:
cpp
template<class T, class Ref, class Ptr>
struct ListIterator {
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node;
ListIterator(Node* node) : _node(node) {}
// *it ------ 返回 Ref(可能是 T& 或 const T&)
Ref operator*() { return _node->_data; }
// it-> ------ 返回 Ptr(可能是 T* 或 const T*)
Ptr operator->() { return &_node->_data; }
// ++it
Self& operator++() { _node = _node->_next; return *this; }
Self operator++(int) { Self tmp(*this); _node = _node->_next; return tmp; }
// --it
Self& operator--() { _node = _node->_prev; return *this; }
Self operator--(int) { Self tmp(*this); _node = _node->_prev; return tmp; }
bool operator!=(const Self& it) { return _node != it._node; }
bool operator==(const Self& it) { return _node == it._node; }
};
实例化时传入不同的 Ref / Ptr:
cpp
typedef ListIterator<T, T&, T*> iterator; // 可读可写
typedef ListIterator<T, const T&, const T*> const_iterator; // 只读
// iterator 的 operator* 返回 T&(可修改内容)
// const_iterator 的 operator* 返回 const T&(只读)
// 一份模板代码同时生成两个迭代器类型,精简而优雅!
10.3 list 容器框架(_size 成员变量)
cpp
template<typename T>
class MyList {
private:
typedef ListNode<T> Node;
Node* _head; // 哨兵位头节点
size_t _size; // 元素个数
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
// ... 成员函数见下文
};
10.4 empty_init() ------ 统一的初始化函数
课堂代码将初始化逻辑抽取为一个独立的辅助函数,这样所有构造函数都可以复用:
cpp
void empty_init() {
_head = new Node;
_head->_next = _head; // 循环------空链表时头节点自指
_head->_prev = _head;
_size = 0;
}
为什么需要 empty_init()? 默认构造和拷贝构造都需要相同的初始化逻辑,抽出来既避免重复代码,也保证一致性。
cpp
list() { empty_init(); }
list(const list<T>& lt) {
empty_init();
for (auto& e : lt) push_back(e);
}
10.5 核心:insert 和 erase
这是 list 所有操作的基础------push_back、push_front、pop_back、pop_front 甚至 clear 都可以基于它们实现。掌握了这两个函数,就掌握了 list 模拟实现的 80%。
⚠️ 课堂差异 :老师为了教学简化,将
insert的返回类型设为void(不返回值),而标准库的insert返回指向新插入元素的迭代器。以下代码同时给出两种形态,但课堂版本是void insert()。
cpp
// insert(课堂版):在 pos 之前插入 val,不返回迭代器
void insert(iterator pos, const T& val) {
Node* cur = pos._node; // 获取当前迭代器指向的节点
Node* newnode = new Node(val); // 创建新节点
Node* prev = cur->_prev; // 找到前驱节点
// 建立链接:prev <-> newnode <-> cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
_size++; // ✅ 每插入一个元素,计数+1
}
// erase:删除 pos 指向的节点,返回下一个有效节点的迭代器
iterator erase(iterator pos) {
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
// 跳过当前节点:prev <-> next
prev->_next = next;
next->_prev = prev;
delete cur; // 释放被删节点
_size--; // ✅ 每删除一个元素,计数-1
return iterator(next); // 返回下一个节点,用于更新迭代器
}
指针操作示意图(以 insert 为例):
插入前:prev <-> cur
插入后:prev <-> newnode <-> cur
只需修改 4 个指针,不移动任何数据------这就是 list 的 insert/erase 为 O(1) 的原因。
10.6 基于 insert/erase 的派生操作
课堂代码中同时保留了两种风格的 push_back------先展示手动版(便于理解指针操作),再推荐重用版(简洁且不易出错):
cpp
// 手动版 push_back(注释保留,用于教学对比)
/* void push_back(const T& x) {
Node* newnode = new Node(x);
Node* tail = _head->_prev;
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
} */
// 推荐版 ------ 复用 insert 和 erase
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()); }
10.7 构造 & 析构(深拷贝 + Copy and Swap)
课堂实现强调了一个重要原则:"需要析构,一般就需要自己写深拷贝;不需要析构,一般就不需要自己写深拷贝,默认浅拷贝就可以。" 因为 list 在堆上管理节点,必须深拷贝确保每个实例拥有独立的节点。
cpp
// 默认构造(复用 empty_init)
list() { empty_init(); }
// 拷贝构造 ------ 先初始化再逐个 push_back
list(const list<T>& lt) {
empty_init();
for (auto& e : lt) push_back(e);
}
// 拷贝并交换(Copy and Swap)
void swap(list<T>& lt) {
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
list<T>& operator=(list<T> lt) {
swap(lt);
return *this;
}
// clear ------ 课堂版本复用 erase
void clear() {
iterator it = begin();
while (it != end()) {
it = erase(it); // 正好也演示了 erase 更新迭代器的用法
}
}
// 析构
~list() {
clear();
delete _head;
_head = nullptr;
}
10.8 begin() / end() ------ 简洁的隐式转换写法
老师的 begin() 充分利用了迭代器的单参构造函数,直接返回 Node*------通过隐式类型转换自动构造成 iterator:
cpp
iterator begin() {
return _head->_next; // ✅ 隐式转换:Node* → iterator
// 等价于:return iterator(_head->_next);
}
iterator end() {
return _head; // ✅ end() 返回哨兵头节点自身
}
const_iterator begin() const { return _head->_next; }
const_iterator end() const { return _head; }
size_t size() const { return _size; } // O(1),有 _size 成员
bool empty() { return _size == 0; }
10.9 完整课堂源码(namespace bit + 三个测试用例)
下面给出老师的全部代码,包含三个精心设计的测试函数:
cpp
#pragma once
#include <assert.h>
#include <iostream>
using namespace std;
namespace bit
{
// ===== 节点设计 =====
template<class T>
struct ListNode {
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
ListNode(const T& x = T())
: _next(nullptr), _prev(nullptr), _data(x)
{}
};
// ===== 迭代器封装(三模板参数) =====
template<class T, class Ref, class 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) { return _node != it._node; }
bool operator==(const Self& it) { return _node == it._node; }
};
// ===== list 容器 =====
template<class T>
class list {
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
// ----- 迭代器接口 -----
iterator begin() { return _head->_next; }
iterator end() { return _head; }
const_iterator begin() const { return _head->_next; }
const_iterator end() const { return _head; }
// ----- 构造 / 析构 -----
void empty_init() {
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
list() { empty_init(); }
list(const list<T>& lt) {
empty_init();
for (auto& e : lt) push_back(e);
}
void swap(list<T>& lt) {
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
list<T>& operator=(list<T> lt) {
swap(lt);
return *this;
}
void clear() {
iterator it = begin();
while (it != end()) {
it = erase(it);
}
}
~list() {
clear();
delete _head;
_head = nullptr;
}
// ----- 增删操作 -----
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()); }
void insert(iterator pos, const T& val) {
Node* cur = pos._node;
Node* newnode = new Node(val);
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
_size++;
}
iterator erase(iterator pos) {
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
_size--;
return iterator(next);
}
// ----- 容量 -----
size_t size() const { return _size; }
bool empty() { return _size == 0; }
private:
Node* _head;
size_t _size;
};
// ==========================================
// 测试函数 1 ------ 基础功能(修改+遍历)
// ==========================================
void test_list1() {
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list<int>::iterator it = lt.begin();
while (it != lt.end()) {
*it += 10; // ✅ 测试通过迭代器修改元素
cout << *it << " ";
++it;
}
cout << endl;
lt.push_front(10);
lt.push_front(20);
lt.push_front(30);
for (auto e : lt) {
cout << e << " ";
}
cout << endl;
lt.pop_back();
lt.pop_back();
lt.pop_front();
lt.pop_front();
for (auto e : lt) {
cout << e << " ";
}
cout << endl;
}
// ==========================================
// 测试函数 2 ------ operator-> 与自定义类型
// ==========================================
struct A {
int _a1;
int _a2;
A(int a1 = 0, int a2 = 0) : _a1(a1), _a2(a2) {}
};
void test_list2() {
list<A> lt;
A aa1(1, 1);
A aa2 = {1, 1};
lt.push_back(aa1);
lt.push_back(aa2);
lt.push_back(A(2, 2));
lt.push_back({3, 3}); // 隐式类型转换
lt.push_back({4, 4});
list<A>::iterator it = lt.begin();
while (it != lt.end()) {
// 三种等价写法
// cout << (*it)._a1 << ":" << (*it)._a2 << endl; // 写法1
cout << it->_a1 << ":" << it->_a2 << endl; // 写法2 ✅ 推荐
cout << it.operator->()->_a1 << ":" // 写法3(本质)
<< it.operator->()->_a2 << endl;
++it;
}
cout << endl;
}
// ==========================================
// 测试函数 3 ------ const 迭代器与拷贝构造
// ==========================================
void PrintList(const list<int>& clt) {
// 这里必须使用 const_iterator
list<int>::const_iterator it = clt.begin();
while (it != clt.end()) {
// *it += 10; // ❌ 编译错误!const_iterator 不能修改
cout << *it << " ";
++it;
}
cout << endl;
}
void test_list3() {
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
PrintList(lt); // 传 const 引用
list<int> lt1(lt); // 拷贝构造
PrintList(lt1);
}
}
// ===== 运行所有测试 =====
int main() {
cout << "=== test_list1 ===" << endl;
bit::test_list1();
cout << "\n=== test_list2 ===" << endl;
bit::test_list2();
cout << "\n=== test_list3 ===" << endl;
bit::test_list3();
return 0;
}
测试输出:
=== test_list1 ===
11 12 13 14 15
30 20 10 11 12 13 14 15
11 12 13
=== test_list2 ===
1:1
1:1
2:2
2:2
3:3
3:3
4:4
4:4
=== test_list3 ===
1 2 3 4 5
1 2 3 4 5
10.10 课堂实现要点总结
| 设计点 | 说明 |
|---|---|
namespace bit |
课堂命名空间,避免与 std 冲突 |
empty_init() |
哨兵头初始化函数,被多个构造复用 |
size_t _size |
显式跟踪元素个数,保证 size() 为 O(1) |
void insert() |
老师为教学简化,标准库版本返回迭代器 |
erase() 返回值 |
指向被删节点下一个位置,用于更新迭代器 |
clear() 复用 erase() |
通过 it = erase(it) 方式逐个删除,展示迭代器更新 |
| Copy and Swap | 赋值重载采用现代写法,异常安全 |
ListConstIterator → 三模板参数 |
展示了设计的进化过程 |
十一、最佳实践 & 常见陷阱
11.1 不要用 std::sort 对 list 排序
cpp
// 错误!std::sort 需要随机访问迭代器
// sort(lst.begin(), lst.end()); // 编译错误!
// 正确:使用 list 自己的 sort 成员函数
lst.sort();
11.2 splice 是 list 独有的高效操作
splice 只修改指针,不拷贝数据。当你需要将一批数据从一个容器转移到另一个容器且不需要保留原数据时,splice 远比 insert 高效。
cpp
// 差:拷贝数据
l1.insert(l1.end(), l2.begin(), l2.end()); // O(n) 拷贝
l2.clear();
// 好:转移节点
l1.splice(l1.end(), l2); // O(1)
11.3 unique 前先 sort
cpp
list<int> lst = {1, 2, 1, 3, 2, 3};
lst.unique(); // ❌ 无效!因为重复元素不相邻
// lst = {1, 2, 1, 3, 2, 3}
lst.sort(); // 先排序
lst.unique(); // 再去重
// lst = {1, 2, 3}
11.4 频繁查找不要用 list
cpp
// list 查找需要 O(n)
list<int> lst = {1, 2, 3, ..., 10000};
auto it = find(lst.begin(), lst.end(), 8888); // 要遍历!O(n)
// 如果查找很频繁,考虑 set 或 unordered_set
11.5 注意内存开销
每个 list 节点都有两个指针的开销。对于存储小类型(如 char、int),list 的内存效率远不如 vector:
cpp
// vector<char>:每个元素占 1 字节
// list<char>:每个元素占 1 字节 + 2 个指针(16 字节在 64 位系统)
// 内存开销扩大了 17 倍!
11.6 erase 后及时更新迭代器
综合第八章的讨论,无论是 list 还是 vector,erase 后都建议用返回值更新迭代器:
cpp
it = lst.erase(it); // 通用的安全写法
十二、总结
12.1 核心知识速查表
| 分类 | 重点内容 |
|---|---|
| 底层结构 | 带头双向循环链表,哨兵位头节点 |
| 构造 | 5 种构造方式,与 vector 类似 |
| 迭代器 | 双向迭代器,不支持 +n / [] |
| 增删 | 任意位置 O(1),insert/erase 为核心 |
| 独有操作 | splice、remove、unique、sort、merge、reverse |
| 迭代器失效 | 仅 erase 使被删节点失效,比其他容器宽松得多 |
| 模拟实现 | 节点 + 迭代器封装(三模板参数)+ empty_init()/_size/深拷贝/Copy-and-Swap |
| 课堂要点 | void insert(教学简化)、迭代器设计进化史(ListConstIterator → 三模板)、clear() 复用 erase() |
12.2 vector vs list vs deque 三容器选择指南
| 场景 | 推荐容器 | 原因 |
|---|---|---|
| 频繁随机访问 | vector |
O(1) 随机访问 |
| 仅在尾部增删 | vector |
尾部 O(1),连续内存缓存友好 |
| 频繁在中间插入/删除 | list |
任意位置 O(1) |
| 需要在头尾增删 | deque |
头尾 O(1),且支持随机访问 |
| 需要大块连续内存 | vector |
内存利用率最高 |
| 需要稳定的迭代器(插入不失效) | list |
链表特性决定 |
12.3 推荐进一步学习
- cppreference.com --- std::list
- 《STL 源码剖析》------ 侯捷(带你手撕 list 源码)
- C++ Core Guidelines: 容器选择
- 对比阅读:[vector 详解:从入门到模拟实现](你之前通过的 vector 博客)
如果这篇博客对你有帮助,欢迎点赞、收藏,也欢迎在评论区指出任何问题或讨论~
完稿日期:2026年5月5日