一. list是什么
继 string 与 vector 之后,我们终于来到了 STL 中另一位成员 list。它的底层实现基于双向链表这一经典的数据结构。
1. 什么是链表
链表是一种将数据通过指针连接在一起的数据结构。它不像数组那样将所有元素保存在连续的内存中,而是每一个元素也就是结构体(称为节点)都通过指针指向下一个节点
以双向链表为例,我们通常这样定义一个节点:
template<class T>
struct listNode
{
ListNode(const T& data = T())
: _data(data)
, _prev(nullptr)
, _next(nullptr)
{}
T _data; // 存储实际内容
listNode<T>* _prev; // 指向前一个节点的指针
listNode<T>* _next; // 指向下一个节点的指针
}

list 的典型特性:
-
插入 / 删除效率高:O(1) 时间复杂度,只需要删除节点并修改指针即可
-
不支持随机访问:不能像数组一样使用下标,list[i]
-
支持双向迭代器(可以从前往后,也可以从后往前遍历)
-
每次访问某个位置都要从头到尾一个个跳过去
2. list的迭代器失效问题
实现 vector 时经常会遇到迭代器失效问题,这是因为 vector 本质上是一段连续的内存空间,当触发扩容或缩容时,指向原内存空间的迭代器都会失效。
但是 list 的实现原理则完全不同,由于其本质上是一个双向链表,每个节点都是一个独立的内存块通过指针连接在一起,由于内存空间不连续,插入或删除节点时只需调整相邻节点的指针,其他节点的地址保持不变,因此指向这些节点的迭代器依然有效。

删除 node2 节点时,只需调整 node1 和 node3 的指针即可。指向这两个节点的迭代器不会失效,因为它们的地址并没有变。
3. 什么是哨兵位
在链表中,哨兵位是一个特殊的“辅助节点”,它不存储用户的真实数据,仅作为边界标记存在。
通常的设计是:
-
空链表:哨兵的前驱和后继都指向自己。
-
非空链表:第一个元素的前驱、最后一个元素的后继都指向哨兵,从而把链表首尾“闭合”起来。
在 STL 的 list 实现中,哨兵就是 _head 节点,它始终存在,永远不会被删除。

二. list的模拟实现
模拟实现 list 的基本成员:
template<class T>
struct listNode
{
// 构造函数:创建节点时可传入数据,默认为 T()(值初始化)
listNode(const T& data = T())
: _data(data), _prev(nullptr), _next(nullptr)
{}
T _data; // 节点存储的数据
listNode<T>* _prev; // 指向前驱节点
listNode<T>* _next; // 指向后继节点
};
template<class T>
class list
{
typedef listNode<T> Node; // 节点类型别名
public:
// 初始化空链表:建立一个哨兵节点(环形结构)
// 空链表时 _head 的前驱和后继都指向自己
void empty_init()
{
_head = new Node;
_head->_prev = _head->_next = _head;
_size = 0;
}
// 尾插:在链表尾部插入一个新节点
void push_back_node(const T& val = T())
{
// 如果链表还没有初始化,先构建一个空链表
if(_head == nullptr)
{
empty_init();
}
// 创建新节点
Node* newNode = new Node(val);
// 获取尾节点:由于是环形,尾节点就是 _head->_prev
Node* tail = _head->_prev;
// 插入到尾部(tail 和 _head 之间)
newNode->_prev = tail;
newNode->_next = _head;
tail->_next = newNode;
_head->_prev = newNode;
++_size; // 更新链表大小
}
private:
Node* _head = nullptr; // 头结点(哨兵位,永远存在)
size_t _size = 0; // 链表中元素个数
};
list 的基本成员相对简洁, 只需要一个头结点即可, 作为整个链表的访问入口
1. 构造函数

1.1 默认构造
初始化一个空环,只有哨兵位,将 prev 和 next 指针都指向自己
list()
{
empty_init();
}
1.2 填充构造
指定 n 个空间并用 val 初始化
list(size_t n, const T& val = T())
{
empty_init();
while(n--)
{
push_back_node(val); // 循环尾插 n 个节点
}
}
1.3 区间构造
实现逻辑基本和填充构造相同
template<class InputIterator>
list(InputIterator first, InputIterator last)
{
empty_init();
while(first != last)
{
push_back_node(*first); // 尾插一个节点
first++; // 移动到下一个元素
}
}
1.4 拷贝构造
list(const list<T>& lt)
{
empty_init();
for(auto& e : lt)
{
push_back_node(e);
}
}
1.5 初始化列表构造(C++11 新增)
在 C++11 之前,如果我们想用一组数据初始化一个容器,只能先建空容器,再手动一次次插入,非常繁琐。
所以C++11 引入了 初始化列表构造,例如:
list<string> names{"Alice", "Jack", "Tom"};
这样我们使用花括号就可以初始化容器,避免一次次的插入,并且在我们阅读代码的时候可以立刻看到容器里面的值,大大提升可读性
list(initializer_list<T> lt)
{
empty_init();
for(auto& e : lt)
{
push_back_node(e);
}
}
这个构造函数可以理解为 C++11 引入的语法糖,本质是用 initializer_list 来统一支持 大括号初始化。让容器初始化变得更优雅
1.6 析构函数
在带头双向链表中,析构函数的任务就是逐个释放所有节点的内存,最后释放哨兵节点,把整个链表彻底清空。
~list()
{
if(_head == nullptr)
return;
Node* cur = _head->next;
while(cur != _head)
{
Node* next = cur->_next; // 从 head->_next 开始遍历,直到回到哨兵节点
delete cur; // 删除所有有效节点
cur = next; // 更新cur
}
delete _head; // 最后删除哨兵节点
_head = nullptr;
_size = 0;
}
2. 迭代器
首先需要了解一个前提,为什么list的迭代器需要用类来实现而不是像vector一样使用指针呢
1) 底层布局不同:连续 vs 非连续
vector底层是连续内存。元素在内存中紧挨着,指针相加减就能到相邻元素,所以随机访问也成立。而 list 底层是由双向链表实现的非连续内存。相邻元素靠指针链接而不是地址相邻,所以用 T* 根本无法找到下一个或上一个元素,只能通过节点指针跳转。
2) 迭代器语义不同:随机访问 vs 双向
vector 迭代器属于随机访问迭代器,天然等价于 T*(支持 it + n,it [n]),而list迭代器是双向迭代器,只要求 ++ 和 --,不支持加减偏移、下标访问。我们需要把向前,向后跳转实现为 node->prev / node->next,这就必须封装一个类来保存节点指针 node* 并定义 operator++/--。
2.1 模拟实现
template<class T>
class Iterator
{
typedef ListNode<T> Node;
typedef Iterator<T> Self;
Node* _node;
Iterator(Node* node)
:_node(node)
{}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &(_node->_data);
}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self operator++(int)
{
Self tmp(_node); // 后置++,先把自增前的内容存起来
_node = _node->_next;
return tmp;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
Self operator--(int)
{
Self tmp(_node); // 后置--,先存储
_node = _node->_prev;
return tmp;
}
// 比较底层节点的地址
bool operator==(const Self& it) { return _node == it._node; }
bool operator!=(const Self& it) { return _node != it._node; }
}
2.2 const迭代器
在 STL 容器中,几乎所有容器都需要同时提供 iterator 和 const_iterator (只读元素),如果每个都手写上面一套 operator*,operator->,++/--,代码会有大量重复和冗余,并且可读性也很低。
我们可以通过添加模板参数的方式来复用上面所写的代码:
template<class T, class Ref, class Ptr>
class Iterator
{
typedef ListNode<T> Node;
typedef Iterator<T, Ref, Ptr> Self;
Node* _node;
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &(_node->_data);
}
Self& operator++()
{
_node = _node->_next;
return *this;
}
//...
}
在 List 类中,我们可以这么定义迭代器类型:
template<class T>
class List
{
typedef Iterator<T, T&, T*> iterator;
typedef Iterator<T, const T&, const T*> const_iterator;
}
这样,我们就完美的完成了代码的复用,一份代码同时实现了 iterator 和 const_iterator
2.3 反向迭代器
list 和其他 STL 容器一样,也提供反向迭代器用于从尾到头遍历。反向迭代器本质上是对正向迭代器的一个包装器,反向迭代器的前进操作对应底层正向迭代器的后退操作,反之亦然。
反向迭代器内部保存的是基迭代器,指向正向序列中当前元素的下一个位置(例如 rbegin() 的基迭代器就是正向迭代器的 end() )。因此,反向迭代器在实现解引用时,必须先向前移动一个位置再返回该位置的元素引用

template <class Iterator, class Ref, class Ptr>
class reverseIterator
{
typedef reverseIterator<Iterator, Ref, Ptr> Self;
Iterator _it;
reverseIterator(Iterator it)
: _it(it)
{}
Ref operator*()
{
Iterator tmp = _it;
return *(--tmp);
}
Ptr operator->()
{
// 使用addressof无条件取地址,避免因为&运算符重载而取到其他值
return std::addressof(operator*());
}
Self& operator++()
{
--_it; // 反向迭代器自增等于正向迭代器自减
return *this;
}
Self operator++(int)
{
Iterator tmp = _it;
++_it;
return tmp;
}
Self& operator--() { ++_it; return *this; }
Self operator--(int) { Iterator tmp = _it; ++_it; return tmp; }
// 按值比较调用正向迭代器的 operator== operator!=
bool operator==(const Self& rit) const { return _it == rit._it; }
bool operator!=(const Self& rit) const { return _it != rit._it; }
}
同样,在 List 中可以这么定义:
template<class T>
class List
{
typedef Iterator<T, T&, T*> iterator;
typedef Iterator<T, const T&, const T*> const_iterator;\
typedef reverseIterator<iterator, T&, T*> reverse_iterator;
typedef reverseIterator<const_iterator, const T&, const T*>
const_reverse_iterator;
}
2.4 迭代器相关接口

iterator begin() { return iterator(_head->_next); }
iterator end() { return iterator(_head); }
const_iterator begin() const { return const_iterator(_head->_next); }
const_iterator end() const { return const_iterator(_head); }
因为哨兵位是无效节点,所以 begin 返回的应是下一个节点

reverse_iterator rbegin() { return reverse_iterator(end()); }
reverse_iterator rend() { return reverse_iterator(begin()); }
const_reverse_iterator crbegin() const { return const_reverse_iterator(end()); }
const_reverse_iterator crend() const { return const_reverse_iterator(begin()); }
3. 增删查改
3.1 insert / erase
在指定位置插入数据以及删除数据

insert:在指定迭代器的位置之前插入数据
iterator insert(iterator pos, const T& val = T())
{
Node* newNode = new Node(val);
Node* cur = pos._node; // 先记录下来迭代器的指向
Node* prevNode = cur->_prev;
// 插入的逻辑实在迭代器指向的节点前插入
newNode->_prev = prevNode;
newNode->_next = cur;
prevNode->_next = newNode;
cur->_prev = newNode;
++_size;
return iterator(newNode); // 返回新节点的位置
}
erase:删除指定迭代器指向的数据
iterator erase(iterator pos)
{
assert(pos != end()); // 先确保删除的节点不是end()无效节点
Node* cur = pos._node;
Node* prevNode = cur->_prev;
Node* nextNode = cur->_next;
prevNode->_next = nextNode;
nextNode->_prev = prevNode;
delete cur;
--_size;
return iterator(nextNode);
}
insert / erase 的返回值
在容器操作里插入或删除元素后,原迭代器可能失效。即便是 list 能保证大多数迭代器有效,但指向被删元素的迭代器一定失效。所以标准库约定 insert 返回指向新插入元素的迭代器,erase 返回指向删除位置后一个元素的迭代器。这样用户就能顺着返回值继续走操作,而不必重新定位。
有了返回值我们就可以这样写
// 连续插入,insert的返回值充当下一个位置
auto it = list.insert(pos, 10);
it = list.insert(it, 44); // 可以在接下来的位置继续插入
// 连续删除,每一次删除后使用erase的返回值继续前进
for(auto it = list.begin(); it != end(); )
{
if(*it % 2 == 0)
it = list.erase(it); // 删除并获得下一个元素的位置
else
++it;
}
如果没有返回值,我们就必须自己手动 find 新位置,既麻烦又容易用到了已失效的迭代器而导致崩溃。
3.2 头插/删,尾插/删

分别在头部插入/删除,尾部插入/删除,直接复用insert/erase即可
// 调用 insert 在 end() 位置插入,相当于尾插
void push_back(const T& val = T()) { insert(end(), val); }
void pop_back() { erase(--end()); }
// 调用 insert 在 begin() 位置插入,相当于头插
void push_front(const T& val = T()) { insert(begin(), val); }
void pop_front() { erase(begin()); }
3.3 容量操作

模拟实现也很简单:
bool empty() { return _size == 0; }
size_t size() { return _size; }
// 系统允许的最大size_t值除以单个节点大小
size_t max_size() { return SIZE_MAX / sizeof(Node); }
3.4 其他接口
splice:常数时间拼接
把整段节点从一个 list 搬到另一个(或同一个)里,不拷贝元素和分配内存,只是改指针
std::list<int> a{1,2,3}, b{7,8,9};
auto pos = std::next(a.begin()); // 让迭代器后移一步指向 2
a.splice(pos, b); // 把 b 全部接到 2 前:a= {1,7,8,9,2,3}
remove / remove_if: 按值/条件删除
list 自带按值/按条件删除。
bool single_digit (const int& value) { return (value < 10); }
lst.remove(0); // 删除所有等于 0 的元素
lst.remove_if(single_digit); // 删除所有10以下的元素
a.sort(); b.sort();
a.merge(b); // a 变成合并后的有序序列,b 清空
unique:去重相邻元素
仅移除相邻的重复元素
std::list<int> lst{1,1,2,2,2,3,1};
lst.unique(); // -> {1,2,3,1}(只删除相邻的)
sort / merge / reverse:链表特有整形操作
sort :对 list 原地排序,不需要额外内存
List<int> list = {1, 3, 5, 2, 4};
lst.sort(); // list = {1, 2, 3, 4, 5}
merge:把已排序的链表合并到 *this 并保持有序,节点级拼接。
a.sort(); b.sort();
a.merge(b); // a 变成合并后的有序序列,b 清空
reverse:就地反转链表,改指针即可。
List<int> list = {1, 2, 3, 4, 5};
mylist.reverse(); // list = 5 4 3 2 1
这些操作都不整体失效已有迭代器;被搬迁/被删除节点的迭代器按各自语义处理。
emplace / emplace_back / emplace_front:原地构造
省一次临时对象的拷贝/移动,构造开销大的类型更有价值
struct Person { std::string name; int age; };
std::list<Person> ps;
ps.emplace_back("Alice", 20); // 直接在尾部构造
ps.emplace(ps.begin(), "Bob", 30); // 在迭代器前原地构造
assign / swap / clear / resize
-
assign(n, val) / assign(first, last):重置内容。
-
swap:常数时间交换两个 list 的内部指针。
-
clear:删除所有元素(但保留内部结构/哨兵)。
-
resize(n, val)扩张补元素 / 收缩删尾部。
3.5 查找元素:find
list 没有像 vector 那样的 find 成员函数,是因为链表不能随机访问,而且 STL 的设计理念是容器只提供最基本的管理接口,把算法独立出来。查找元素时使用 <algorithm> 里的 std;;find 即可。
标准库里的 find 采用了线性查找:
template<class InputIterator, class T>
find(Iterator first, Iterator last, const T& val)
{
while(first != last)
{
if(*first == val)
return first; // 找到了直接返回
++first;
}
return last; // 找不到返回last
}
从 first 走到 last,遇到匹配值就返回迭代器,否则返回 last。
三. 完整接口一览表
| 类别 | 函数名 / 操作符 | 说明 |
|---|---|---|
| 构造函数 | list(), list(size_t n, const T& val), list(It first, It last), list(std::initializer_list<T>) | 默认、填充、迭代器区间、初始化列表构造 |
| 拷贝 移动构造 | list(const list&), list(list&&) | 深拷贝构造 / 移动构造(转移节点指针) |
| 赋值操作 | operator=, assign(n, val), assign(first, last), assign(init_list) | 重置内容:按个数 / 区间 / 初始化列表 |
| 容量管理 | size(), empty(), max_size() | 查询当前元素个数、是否为空、理论最大量(与分配器/节点大小相关) |
| 元素访问 | front(), back()(及 const 版本) | 访问首/尾元素 |
| 迭代器 | begin(), end(), cbegin(), cend(), rbegin(), rend(), crbegin(), crend() | 正向/反向迭代器(list 为双向迭代器) |
| 增加元素 | push_front(const T&), push_back(const T&), emplace_front(args...), emplace_back(args...), insert(pos, const T&), emplace(pos, args...) | 头/尾插与原地构造;在 pos 前插入/原地构造 |
| 删除元素 | pop_front(), pop_back(), erase(pos), erase(first, last), clear(), resize(n, val=T()) | 头/尾删;删单个或区间;清空;调整大小 |
| 列表特有操作 | splice(pos, list& other), splice(pos, list& other, it), splice(pos, list& other, first, last) | 常数时间拼接/搬移节点(不拷贝元素) |
| 内容清理 | remove(const T&), remove_if(UnaryPred), unique(), unique(BinaryPred) | 按值/按谓词删除;相邻去重(常与 sort 配合) |
| 序列整形 | sort(), sort(Compare), merge(list& other), merge(list& other, Compare), reverse() | 原地稳定排序;合并有序链表;反转 |
| 比较操作 | ==, !=, <, <=, >, >= | 词典序比较 |
| 交换操作 | swap(list& other) | 交换两个链表(指针层面,常数时间) |
| 查找(算法) | std::find(first, last, val)(非成员) | list 不提供成员 find;使用 <algorithm> 通用算法 |
四. 总结
本文从“list 是什么”讲起,展示了如何用代码模拟实现 list 的构造、容量管理、增删查改和迭代器封装。相比 vector,list 基于双向链表,插入、删除在常数时间完成,不需要移动元素;但不支持随机访问,迭代器只支持双向遍历。通过这些实现,我们可以理解 std;;list 的常用接口和特点,知道何时选择链表这种容器。

9913

被折叠的 条评论
为什么被折叠?



