c++oding="utf-8" ?>
std::forward_list 没有 size() 是标准刻意设计,为避免维护计数器的开销;c++17 前 size() 为 o(n),c++17 起要求 o(1) 但可移植代码须按无 size() 编写,判空用 empty()。

std::forward_list 不是“更轻量的 std::list 替代品”,它是为特定极简场景设计的单向链表——用对了能省 8 字节/节点,用错了连 size() 都会拖慢整个循环。
为什么 std::forward_list 没有 size() 成员函数
不是忘了加,是标准刻意不提供。C++11 到 C++14 的 std::forward_list::size() 是 O(n) 实现(遍历计数),而标准要求“零开销抽象”:每个插入/删除操作都不该为维护一个计数器多执行一次 +1/-1。所以它压根不存 size_ 字段。
C++17 起才强制要求 size() 为 O(1),但很多嵌入式 STL(如旧版 libstdc++)或裁剪版仍不实现。可移植代码必须按“无 size()”来写:
- 判空永远用
fl.empty(),别写fl.size() == 0 - 循环中禁止写
for (size_t i = 0; i —— 这是 O(n²) - 真要长度?用
std::distance(fl.begin(), fl.end()),但只调一次,缓存结果 - 若频繁需要长度,说明你选错了容器,该换
std::list或带计数器封装
insert_after 和 erase_after 是唯一合法的中间修改方式
单向链表没有前驱指针,无法从某个 it 推出它的上一个节点。所以 fl.insert(it, val) 和 fl.erase(it) 根本不存在,编译直接失败。
所有中间插入/删除都必须明确告诉容器:“在谁后面干”:
-
fl.insert_after(fl.before_begin(), val)等价于push_front() - 想在第 3 个元素后插入?先
auto it = std::next(fl.begin(), 2),再fl.insert_after(it, val) -
fl.erase_after(fl.before_begin())删除首节点,等价于pop_front() -
fl.erase_after(it)中的it不能是fl.end()—— 它不是有效节点,行为未定义
splice_after() 是唯一真正 O(1) 的拼接操作
std::forward_list 不支持 splice(),只有 splice_after()。但它拼接不拷贝、不构造、不析构,只改几个 next 指针 —— 这是它不可替代的核心价值。
典型用法:
-
dst.splice_after(pos, src):把整个src拼到dst的pos后,src变空 -
dst.splice_after(pos, src, it):只把src中it所指节点移到dst的pos后 -
src和dst必须同类型,且不能是同一个对象(自拼接未定义) - LRU 缓存淘汰、任务队列迁移等高频重组场景,就靠这个避免内存分配和元素移动
什么时候该用 std::forward_list,而不是 list 或 vector
它不是“链表通用解”,而是窄口径工具:
- 适用:内存极度敏感(比如百万级
int节点)、只从前向后遍历、高频头插/头删、配合哈希表做节点索引(如 LRU) - 不适用:需要
back()、需要operator[]、需要std::sort(得用fl.sort()成员函数)、需要反向迭代、需要频繁查长度 - 误用信号:你写了
std::prev(it)、你反复调size()、你试图封装push_back() - 64 位系统下,每个节点比
std::list少 8 字节 —— 但只在值类型小、节点数大时才显著;存std::string时这 8 字节毫无意义
最易被忽略的一点:它不提供尾指针,也没有 end() 对应的有效插入锚点。所谓“在末尾插入”,本质是找到倒数第二个节点再 insert_after —— 这本身就要 O(n) 遍历。如果你需要尾部操作,别硬扛,换 std::deque 或自己维护尾指针。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











