说到 C++ 的 list,大部分人都知道它底层是双向链表。可真问起来,哨兵节点怎么工作、迭代器失效的坑在哪、指针操作细节,可能马上就会卡壳。面试时手写 List,更是不少人的翻车现场。
我干脆自己写了一个,把一些常被忽略的细节理清楚。
底层结构:带头双向循环链表
std::list 的底层是带头节点的双向循环链表。用一个不存数据的哨兵节点作为锚点,所有插入、删除逻辑都能统一,不用额外判断是不是空链表,也不用单独处理第一个节点或最后一个节点的边界。
节点包含三个部分:前驱指针 _prev、后继指针 _next 和数据域 _data。尾结点的 _next 指向哨兵节点,哨兵节点的 _prev 指向尾结点,形成一个环。这带来的好处是:找尾结点直接 _head->_prev,不用遍历;遍历到尾结点之后,自然又回到哨兵。
和 vector 比,List 的强项是中间位置的插入/删除 O(1),代价是没有随机访问,迭代器更易失效(但只失效被删除的那个),而且每个节点都有指针开销,内存分配分散。
别被
list::sort迷惑,它的排序效率不比算法库的通用排序,如果要对元素排序,先把数据倒进vector再排往往是更好的选择。
实现细节
按节点 → 迭代器 → 容器类的依赖顺序来写,代码都在命名空间 MyList 下。
节点
模板类 list_node 只负责存数据和前后指针,构造函数让指针默认置空,数据用 T() 初始化,这样无论是 int(初始化为 0)还是自定义类型都能正常工作。
#pragma once
#include <iostream>
#include <list>
using namespace std;
namespace MyList {
template<class T>
struct list_node {
list_node<T>* _prev;
list_node<T>* _next;
T _data;
list_node(const T& x = T()) : _prev(nullptr), _next(nullptr), _data(x) {}
};
}
迭代器
List 的迭代器不能是原生指针,因为节点的物理存储不连续。这里用 list_iterator 包装一个 list_node<T>*,通过运算符重载模拟指针行为。
用三个模板参数 T, Ref, Ptr 实现普通 / const 迭代器的复用:Ref 为 T& 时是可读写的迭代器,为 时是只读的; 同理。这样一份代码就能搞定两种迭代器。


