视频加载失败

C++ 中的 list

1041 字
5 分钟
C++ 中的 list

在现代 C++ 开发中,虽然 std::vector 足以应付绝大多数的场景,但是在某些特定场景下,std::list 依旧是不可替代的神器。

核心概念与底层原理#

  • 头文件:#include <list>
  • 本质:双向链表(Doubly Linked List)
  • 内存模型:非连续内存。每个元素(节点)都是独立分配在堆上的,节点之间通过指针(prevnext)连接
  • 特点:
    • 不支持随机访问:不能使用下标 l[5] 访问元素,必须从头一个一个遍历过去
    • 插入 / 删除极快:只要持有了某个位置的迭代器,在该位置插入或删除元素的操作是 O(1)O(1) 的,且不需要移动其他元素

初始化与构造#

用法与 std::vector 非常相似:

#include <list>
std::list<int> l1; // 空链表
std::list<int> l2 = {1, 2, 3}; // 列表初始化
std::list<int> l3(l2); // 拷贝构造
std::list<int> l4(5, 100); // 5 个 元素,全是 100

独有的操作优势(std::vector 做不到的)#

这是 std::list 存在的理由。由于它是链表,它支持一些操作极其高效,或者提供了 std::vector 根本没有的接口。

头部操作#

std::vector 在头部插入 / 删除操作极其低效(O(N)O(N)),而 std::list 则是瞬间完成(O(1)O(1))。

  • push_front(val):头部插入
  • pop_front():头部删除

接合(Splicing)#

可以将一个 std::list 的元素(全部或部分)直接 “剪切” 并 “粘贴” 到另一个 std::list 中,这是纯指针操作,没有数据拷贝,所以速度极快。

std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {4, 5, 6};
auto it = list1.begin();
++it; // 指向 2
// 将 list2 的所有元素 “移动” 到 list1 的 2 前面
// list2 变空,list1 变为 {1, 4, 5, 6, 2, 3}
list1.splice(it, list2);

专用成员函数#

由于 std::list 无法随机访问,标准库算法(如 std::sort)对它无效。因此 list 自带了一套经过优化的成员函数:

  • l.sort():排序(稳定排序,底层通常是归并排序)。注意:千万别读 std::liststd::sort(l.begin(), l.end()),编译会报错
  • l.remove(val):删除所有等于 val 的元素
  • l.remove_if(pred):删除满足条件的元素
  • l.unique():删除相邻的重复元素(去重前通常要先 sort)
  • l.reverse():逆置链表
  • l.merge(l2):合并两个已排序的链表(l2 会变空)

迭代器特性#

这是决定选择 std::vector 还是 std::list 的关键因素。

  • 类型:双向迭代器Bidirectional Iterator
    • 支持:++it--it*it
    • 不支持:it + 5it < other_it(不能跳跃,不能比较大小)
  • 稳定性(Validity):极高
    • 掺入操作:绝对不会让现有的迭代器失效
    • 删除操作:只有指向被删除节点的那个迭代器会失效,其他的完全不受影响
    • 对比 std::vectorstd::vector 一旦扩容,所有迭代器全部失效;中间插入,后面所有迭代器失效。

std::liststd::vector 的选择#

这是一个经典的 Trade-off(权衡)问题:

特性std::vectorstd::list
随机访问O(1)O(1)(支持 []不支持(O(N)O(N)
尾部插入O(1)O(1)O(1)O(1)
头部/中间插入O(N)O(N)(很慢,要挪动数据)O(1)O(1)(很快,前提是有迭代器)
内存布局连续(Cache 命中率高)分散(Cache 命中率低)
额外内存较少(少量预留空间)较大(每个元素都要存两个指针)
迭代器失效容易失效几乎不失效

结论:

  • 95% 的情况用 std::vector:现代 CPU 的缓存机制非常依赖内存连续性。即使是在中间插入,对于小对象(如 int),std::vector 往往也比 std::list 快,因为 std::list 的遍历会导致大量的 Cache Miss。
  • 以下情况可以用 std::list
    1. 需要频繁在头部或中间插入 / 删除,且元素总数很多
    2. 对象非常大,拷贝代价极高(虽然 C++11 移动语义缓解了这个问题)
    3. 核心需求:你需要保存指向某个元素的迭代器,并且在后续操作中(无论怎么插入删除其他元素),希望这个迭代器一直有效

C++11 新增:std::forword_list#

C++11 引入了 std::forword_list(单向链表):

  • 特点:只有 next 指针,没有 prev 指针
  • 优势:比 std::list 更省内存(少存一个指针)
  • 劣势:只能向前遍历,功能受限
  • 场景:极其追求内存优化的哈希表桶实现等

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
C++ 中的 list
https://bfmhno3.github.io/posts/list-in-cpp/
作者
bfmhno3
发布于
2026-01-03
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
bfmhno3
Hello, I'm bfmhno3.
公告
本站曾使用 Jekyll + Minimal Mistakes 主题,现已迁移至 Astro + Firefly。博客内容未变,但文章链接有所调整,请自行查找需要的文章。
分类
标签
最新动态
站点统计
文章
36
分类
2
标签
58
总字数
136,569
运行时长
0
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.16.7
文章许可
CC BY-NC-SA 4.0
文章目录