C++ STL之slist单向链表容器使用方式

发布时间:2026/10/21 5:01:31
C++ STL之slist单向链表容器使用方式 前言先纠正题面里的一个前提slist不是 C 标准库的容器。它是 SGI STLSilicon Graphics 实现也是 libstdc 的重要源头中的单向链表容器属于非标准扩展。C11 引入标准单向链表时采用的名称是std::forward_list定义在forward_list中。因此在标准 C 里写#include slist或std::slist是找不到的部分老工具链的扩展头文件里也许还能见到slist但它的接口、命名空间和可移植性都不由标准保证阅读老代码时要认清这一点。单向链表本身要解决的问题很明确当容器只需要从头向后遍历、并且经常在已知位置之后插入或删除时每个节点只存一个后继指针可以省下可观的链接开销。代价是只能单向走拿不到前驱因此标准库把「在位置 p 插入/删除」重新定义为「在 p 之前的位置插入/删除」——接口长成insert_after、erase_after的样子。本文先说明slist与std::forward_list的对应关系再以标准容器为主线讲接口、迭代器语义、before_begin哨兵、特有的成员算法最后给出选择建议。文中会区分「SGI 扩展的历史写法」和「标准保证的行为」不把扩展当标准推荐。一、从 slist 到 forward_listSGI STL 的slist提供了push_front、pop_front、front、insert_after、erase_after、begin、end、size等成员。它的设计目标是用最小空间实现单链表节点中只有一个next指针。老代码里可能看到这样的写法// 历史写法SGI STL 扩展不是标准 C#include slistint main() {slistint s;s.push_front(1);s.push_front(2); // 2, 1s.insert_after(s.begin(), 3); // 2, 3, 1}这段代码的可用性完全取决于你的实现是否还提供slist头文件与对应命名空间不能作为可移植代码。C11 之后应写成// 适用于 C17#include forward_listint main() {std::forward_listint s;s.push_front(1);s.push_front(2); // 2, 1s.insert_after(s.begin(), 3); // 2, 3, 1}两者的语义高度相似但标准版本有几个明确的差别名字是std::forward_list没有size()成员提供before_begin()与cbefore_begin()哨兵insert_after返回新插入元素的迭代器erase_after返回被删元素之后的迭代器还提供emplace_after、splice_after、merge、sort、unique、remove、remove_if、reverse等成员。为什么标准版不提供size()因为维护一个计数会让某些操作多一次写而forward_list的定位是「零额外空间开销、只保留必要操作」。需要长度时用std::distance(s.begin(), s.end())代价是线性时间如果频繁需要长度说明这个容器多半不合适。二、迭代器与before_begin哨兵forward_list的迭代器是前向迭代器只支持不支持--也不支持随机访问。因此std::sort、std::lower_bound这类需要随机访问或双向移动的算法不能直接用容器自己提供了成员sort()。由于拿不到前驱标准库引入了一个特殊迭代器before_begin()它指向「第一个元素之前」的位置。它不能被解引用只能作为insert_after、erase_after、splice_after的位置参数// 适用于 C17#include forward_list#include iostreamint main() {std::forward_listint fl{1, 2, 3};fl.insert_after(fl.before_begin(), 0); // 等价于 push_front(0)auto it fl.begin(); // 指向 0it fl.insert_after(it, 10); // 0 后插入 10it 指向 10fl.erase_after(it); // 删除 10 之后的元素1for (int v : fl) {std::cout v ; // 按标准语义输出 0 10 2 3}}这里逐条核对语义初始是1 2 3insert_after(before_begin(), 0)变成0 1 2 3it begin()指向 0insert_after(it, 10)在 0 之后插入 10返回指向 10 的迭代器erase_after(it)删除 10 的后继即 1返回指向 2 的迭代器。最终是0 10 2 3。erase_after有两个重载erase_after(pos)删除pos的后继erase_after(first, last)删除(first, last)之间的元素不包含last。后者要求last可达否则是 UB。// 适用于 C17#include forward_list#include iteratorvoid dropFirstTwo(std::forward_listint fl) {// 删除 begin() 起的两个元素范围是 (before_begin, 第三个元素)if (fl.empty()) {return;}auto last fl.begin();std::advance(last, 1); // 指向第二个元素auto stop std::next(last); // 指向第三个元素可能等于 endfl.erase_after(fl.before_begin(), stop);}这段代码删除第 1、2 个元素。若链表只有一个元素std::next(last)得到end()erase_after删除(before_begin, end)之间的全部元素也是正确的。写这类代码时建议把「前驱位置」和「停止位置」在纸上画出来erase_after的区间是左开右开的非常容易差一。三、特有成员算法forward_list把一批链表算法做成了成员函数因为它们需要重新接线通用算法做不到或效率不对成员函数作用复杂度sort()/sort(comp)归并排序稳定O(n log n) 次比较merge(other)合并两个已排序链表O(n m)unique()删除相邻的重复元素O(n)remove(value)删除所有等于 value 的元素O(n)remove_if(pred)删除满足谓词的元素O(n)reverse()就地反转O(n)splice_after(pos, other)把 other 全部接到 pos 后O(1)splice_after(pos, other, it)移动单个元素O(1)splice_after(pos, other, first, last)移动区间O(距离)// 适用于 C17#include forward_list#include functional#include iostreamint main() {std::forward_listint a{3, 1, 4, 1, 5};a.sort(); // 1 1 3 4 5稳定排序a.unique(); // 1 3 4 5std::forward_listint b{2, 6};b.sort();a.merge(b); // a: 1 2 3 4 5 6b 变空a.reverse(); // 6 5 4 3 2 1for (int v : a) {std::cout v ;}}sort()是稳定的这一点与std::sort不同后者不保证稳定。merge之后源链表为空被移动的节点归目标链表所有。splice_after不拷贝、不分配只改指针因此单个元素和整条链的移动是常数时间区间版本要逐段定位代价与区间长度相关。需要特别注意remove与erase的区别remove(value)删除所有等于value的元素erase_after(pos)只删一个位置。remove对forward_list是成员函数因为通用std::remove需要移动元素而链表只需要改指针。四、什么时候该用单向链表场景建议频繁在头部插入删除forward_list合适push_front是 O(1)已持有前驱位置需在其后插入删除insert_after/erase_after是 O(1)需要反向遍历或随机访问用std::list或std::vector需要size()、需要按下标访问用std::vector数据量大且以遍历为主优先std::vector连续内存对缓存友好内存极度敏感、节点数量巨大forward_list每个节点比list少一个指针链表容器的优势在于「已知位置的操作不搬移其他元素」劣势在于每个节点单独分配、遍历时指针跳转缓存局部性差。标准并不保证分配器的具体行为也不保证节点在内存中的相邻关系。因此在没有实际测量的情况下不要断言链表一定比vector快或慢插入删除的复杂度优势只在「已经持有位置」时成立而查找位置本身往往是线性的。常见坑点❌ 写#include slist并期待它是标准容器。✅ 标准写法是#include forward_list与std::forward_listslist是 SGI 扩展。❌ 调用fl.size()获取元素个数。✅forward_list没有size()用std::distance(fl.begin(), fl.end())代价 O(n)。❌ 沿用vector习惯写fl.insert(pos, x)或fl.erase(pos)。✅ 单向链表用insert_after(prev, x)与erase_after(prev)位置参数是前驱。❌ 想删除第一个元素却写fl.erase_after(fl.begin())。✅ 那会删掉第二个元素应写fl.erase_after(fl.before_begin())或pop_front()。❌ 对forward_list调用std::sort(fl.begin(), fl.end())。✅ 用成员fl.sort()通用std::sort需要随机访问迭代器。❌erase_after之后继续使用传入的位置迭代器或已删除元素的迭代器。✅ 使用返回值指向被删元素之后或重新从before_begin()开始定位。❌ 认为splice_after(pos, other, first, last)一定是 O(1)。✅ 单个元素是 O(1)区间版本是 O(区间长度)整条链splice_after(pos, other)是 O(1)。❌ 在循环里对每个元素调用erase_after同时用it推进。✅ 用remove/remove_if或按erase_after的返回值推进避免跳过或重复删除。总结主题结论名称slist是 SGI 扩展标准容器是std::forward_list结构每个节点一个后继指针只能单向遍历位置语义插入删除都以「前驱位置」为参数before_begin()表示头部之前缺失接口没有size()没有反向迭代器没有随机访问专有算法sort、merge、unique、remove、reverse、splice_after选择原则头部操作多、内存敏感时考虑遍历为主时优先连续存储容器把slist当作forward_list的旧称来理解是安全的但不要把旧称写进新代码。真正需要记住的是单向链表的接口哲学没有前驱就用「前一个位置」来表达插入和删除没有长度就不要假设 O(1) 的size()。理解这两点forward_list的一整套_after接口就顺理成章了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询