在 C++ STL 的序列式容器中,list 是与 vector 同样重要的存在。它凭借独特的双向循环链表底层结构,在插入、删除操作上展现出 vector 无法比拟的优势,成为处理 “频繁增删” 场景的首选容器。本文将延续 “能用、明理、能扩展” 的学习思路,从 list 的基础使用入手,深入剖析其底层原理与模拟实现,最后通过与 vector 的全方位对比,帮你精准掌握两者的适用场景。
一、list 基础:认识双向循环链表容器
1.1 什么是 list?
list 的底层是带头结点的双向循环链表,每个节点包含三个部分:
数据域(存储元素值);
前驱指针(指向前一个节点);
后继指针(指向后一个节点)。
这种结构带来两个核心特性:
- 元素不连续存储:节点在内存中分散分布,通过指针串联,无需像 vector 那样维护连续空间;
- 双向遍历:可通过前驱 / 后继指针向前或向后访问节点,支持正向与反向迭代器。
list 的 “头结点” 是一个不存储有效数据的哨兵节点,其作用是统一空容器与非空容器的插入 / 删除逻辑(无需额外判断边界),这也是 STL list 实现的经典设计。
1.2 list 核心接口实战
list 的接口设计与 vector 类似,但部分接口因底层结构差异而有特殊用法。以下按 “构造→迭代器→容量→元素访问→增删改” 的顺序,梳理必须掌握的核心接口。
1.2.1 list 的构造函数
list 提供 4 种常用构造方式,满足不同初始化需求:
| 构造函数声明 | 接口说明 | 代码示例 |
|---|---|---|
list() | 无参构造,创建空 list(仅含头结点) | list<int> l;(空 list,size=0) |
list(size_type n, const value_type& val = value_type()) | 构造含 n 个 val 的 list | list<int> l(5, 3);(5 个 3,size=5) |
list(const list& x) | 拷贝构造,复制另一个 list | list<int> l2(l);(l2 是 l 的副本) |
list(InputIterator first, InputIterator last) | 迭代器构造,复制 [first, last) 区间元素 | int arr[] = {1,2,3}; list<int> l(arr, arr+3);(l 含 1、2、3) |
1.2.2 迭代器:遍历 list 的 “桥梁”
list 的迭代器本质是 “节点指针的封装”,需通过迭代器访问节点数据(无法像 vector 那样随机访问)。核心迭代器接口如下:
| 迭代器接口 | 接口说明 | 代码示例 |
|---|---|---|
begin() + end() | begin():指向第一个有效节点;end():指向头结点(无效位置) | for (auto it = l.begin(); it != l.end(); ++it) { cout << *it << " "; }(正向遍历) |
rbegin() + rend() | rbegin():指向最后一个有效节点(反向迭代器起点);rend():指向头结点(反向迭代器终点) | for (auto it = l.rbegin(); it != l.rend(); ++it) { cout << *it << " "; }(反向遍历) |
关键注意:
list 的
end()指向头结点,而非最后一个有效节点的下一个位置(与 vector 不同);反向迭代器的
++操作等价于正向迭代器的--(例如rbegin()++ 后指向倒数第二个有效节点)。
1.2.3 容量与元素访问
list 的容量接口较简单,仅需关注 “是否为空” 和 “元素个数”;元素访问需通过首尾节点的接口(无随机访问接口):
| 接口类别 | 函数声明 | 接口说明 | 代码示例 |
|---|---|---|---|
| 容量接口 | empty() | 判断 list 是否为空(size==0) | if (l.empty()) { cout << "空list"; } |
size() | 返回有效元素个数(不含头结点) | cout << "元素个数:" << l.size(); | |
| 元素访问 | front() | 返回第一个有效节点的引用 | cout << "第一个元素:" << l.front(); |
back() | 返回最后一个有效节点的引用 | cout << "最后一个元素:" << l.back(); |
注意:list 没有
operator[]和at()接口,无法通过索引访问元素(底层非连续空间,随机访问效率极低)。
1.2.4 增删改:list 的核心优势
list 的插入 / 删除操作是其核心亮点 ——任意位置增删仅需修改指针,无需搬移元素,时间复杂度为 O (1)。核心接口如下:
| 接口类别 | 函数声明 | 接口说明 | 代码示例 |
|---|---|---|---|
| 头部操作 | push_front(val) | 在第一个有效节点前插入 val | l.push_front(0);(头插 0) |
pop_front() | 删除第一个有效节点 | l.pop_front();(头删) | |
| 尾部操作 | push_back(val) | 在最后一个有效节点后插入 val | l.push_back(4);(尾插 4) |
pop_back() | 删除最后一个有效节点 | l.pop_back();(尾删) | |
| 任意位置 | insert(pos, val) | 在 pos 迭代器指向的节点前插入 val | auto pos = find(l.begin(), l.end(), 2); l.insert(pos, 5);(在 2 前插 5) |
erase(pos) | 删除 pos 迭代器指向的节点 | l.erase(pos);(删除 pos 指向的节点) | |
| 其他操作 | swap(list& x) | 交换两个 list 的节点(仅交换头指针,效率高) | l.swap(l2); |
clear() | 清空所有有效节点(保留头结点) | l.clear();(清空后 size=0) |
效率提示:list 的
insert和erase无需扩容(无连续空间限制),也无需搬移元素,这是其与 vector 的核心差异。
1.3 list 的迭代器失效问题(重点)
与 vector 不同,list 的迭代器失效场景非常有限,核心原因是 “底层为链表,增删不影响其他节点的指针”。
哪些操作会导致迭代器失效?
插入操作:不会导致任何迭代器失效。插入新节点仅需修改相邻节点的指针,原有迭代器仍指向原节点(节点未被删除或移动)。
删除操作:仅导致指向被删除节点的迭代器失效,其他迭代器不受影响。因为被删除节点的内存被释放,继续访问会导致非法内存操作。
失效案例与解决办法
错误示例(删除节点后未更新迭代器):
void TestListIteratorError() {
list<int> l{1,2,3,4};
auto it = l.begin();
while (it != l.end()) {
l.erase(it); // 删除it指向的节点后,it失效
++it; // 错误!访问失效的迭代器,程序崩溃
}
}
正确解决办法:利用erase的返回值(返回被删除节点的下一个节点的迭代器),或在删除前提前移动迭代器:
// 方法1:用erase返回值更新迭代器
void TestListIteratorCorrect1() {
list<int> l{1,2,3,4};
auto it = l.begin();
while (it != l.end()) {
it = l.erase(it); // 关键:用返回值更新it,指向删除节点的下一个节点
}
}
// 方法2:删除前提前移动迭代器(it++先返回旧值,再自增)
void TestListIteratorCorrect2() {
list<int> l{1,2,3,4};
auto it = l.begin();
while (it != l.end()) {
l.erase(it++); // 先删除it指向的节点,再让it指向 next 节点
}
}
二、list 深度剖析:模拟实现与反向迭代器
掌握 list 的使用后,我们通过模拟实现核心功能,深入理解其底层逻辑 —— 重点是 “双向循环链表的节点管理” 和 “反向迭代器的设计”。
2.1 模拟实现 list 的核心结构
首先定义 list 的节点结构和容器的核心成员变量:
2.1.1 节点结构(Node)
链表的基础是节点,每个节点需包含数据、前驱指针和后继指针:
namespace bit {
// 定义list的节点结构
template <class T>
struct ListNode {
ListNode<T>* _prev; // 前驱指针
ListNode<T>* _next; // 后继指针
T _data; // 数据域
// 节点构造函数(初始化数据和指针)
ListNode(const T& data = T())
: _prev(nullptr)
, _next(nullptr)
, _data(data)
{}
};
}
2.1.2 list 容器的核心成员
list 容器需维护一个 “头结点”(哨兵节点),通过头结点管理整个链表:
namespace bit {
template <class T>
class list {
typedef ListNode<T> Node; // 简化节点类型名
public:
// 正向迭代器(封装节点指针)
class iterator {
public:
typedef iterator self;
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 temp(*this);
_node = _node->_next;
return temp;
}
// 迭代器--(向前移动)
self& operator--() {
_node = _node->_prev;
return *this;
}
self operator--(int) {
self temp(*this);
_node = _node->_prev;
return temp;
}
// 迭代器比较(判断是否指向同一个节点)
bool operator==(const self& it) const { return _node == it._node; }
bool operator!=(const self& it) const { return _node != it._node; }
Node* _node; // 迭代器底层指向的节点指针
};
// 构造函数(初始化头结点)
list() {
// 创建头结点,初始时前驱和后继都指向自身(双向循环)
_head = new Node();
_head->_prev = _head;
_head->_next = _head;
}
// 尾插接口
void push_back(const T& data) {
Node* new_node = new Node(data);
Node* tail = _head->_prev; // 找到尾节点(头结点的前驱)
// 修改指针:tail <-> new_node <-> _head
tail->_next = new_node;
new_node->_prev = tail;
new_node->_next = _head;
_head->_prev = new_node;
}
// 迭代器接口
iterator begin() { return iterator(_head->_next); } // 第一个有效节点
iterator end() { return iterator(_head); } // 头结点(无效位置)
private:
Node* _head; // 指向头结点的指针
};
}
2.2 反向迭代器的巧妙设计
list 的反向迭代器(reverse_iterator)并非独立实现,而是通过 “封装正向迭代器” 实现 —— 反向迭代器的++等价于正向迭代器的--,反向迭代器的--等价于正向迭代器的++。
这种 “复用正向迭代器” 的设计,避免了代码冗余,是 STL 的经典复用思想:
namespace bit {
// 反向迭代器:模板参数为正向迭代器
template <class Iterator>
class reverse_iterator {
public:
typedef reverse_iterator<Iterator> self;
reverse_iterator(Iterator it) : _it(it) {}
// 反向迭代器解引用(例如rbegin()指向尾节点,需返回尾节点数据)
typename Iterator::reference operator*() {
Iterator temp(_it);
--temp; // 正向迭代器向前移动一位,指向有效节点
return *temp;
}
// 反向迭代器++(实际是正向迭代器--)
self& operator++() {
--_it;
return *this;
}
self operator++(int) {
self temp(*this);
--_it;
return temp;
}
// 反向迭代器--(实际是正向迭代器++)
self& operator--() {
++_it;
return *this;
}
self operator--(int) {
self temp(*this);
++_it;
return temp;
}
// 反向迭代器比较
bool operator==(const self& rit) const { return _it == rit._it; }
bool operator!=(const self& rit) const { return _it != rit._it; }
private:
Iterator _it; // 底层封装的正向迭代器
};
// 在list中添加反向迭代器接口
template <class T>
class list {
// ... 省略之前的代码 ...
public:
typedef reverse_iterator<iterator> reverse_iterator;
reverse_iterator rbegin() { return reverse_iterator(end()); } // end()是头结点,反向迭代器从这开始
reverse_iterator rend() { return reverse_iterator(begin()); } // begin()是第一个有效节点,反向迭代器到这结束
};
}
关键理解:
rbegin()返回的反向迭代器,底层封装的是end()(头结点),解引用时通过--temp指向最后一个有效节点,从而实现反向遍历。
三、list 与 vector 的全方位对比(面试高频)
list 和 vector 是 STL 中最常用的两个序列式容器,但底层结构的差异导致它们的特性和适用场景完全不同。以下从 7 个核心维度进行对比:
| 对比维度 | vector | list |
|---|---|---|
| 底层结构 | 动态顺序表(连续内存空间) | 带头结点的双向循环链表(分散内存节点) |
| 随机访问 | 支持(operator[]/at()),时间复杂度 O (1) | 不支持,需从表头 / 表尾遍历,时间复杂度 O (N) |
| 插入 / 删除效率 | 任意位置插入 / 删除需搬移元素,时间复杂度 O (N);尾插 / 尾删(无扩容)O (1) | 任意位置插入 / 删除仅需修改指针,时间复杂度 O (1) |
| 空间利用率 | 连续空间,不易产生内存碎片;缓存命中率高(局部性原理) | 每个节点需额外存储两个指针,空间开销大;节点分散,缓存命中率低 |
| 迭代器类型 | 原生态指针(T*) | 封装的节点指针(需重载++/--等操作) |
| 迭代器失效 | 1. 插入(扩容时):所有迭代器失效;2. 删除:指向删除位置及之后的迭代器失效 | 1. 插入:无迭代器失效;2. 删除:仅指向被删除节点的迭代器失效 |
| 适用场景 | 1. 需要频繁随机访问元素;2. 插入 / 删除主要在尾部,且元素个数较稳定 | 1. 需要频繁在任意位置插入 / 删除元素;2. 无需随机访问,仅需遍历 |
四、总结
list 作为基于双向循环链表的容器,其核心优势在于 “任意位置的高效增删”,而短板是 “不支持随机访问”。学习 list 的关键在于:
- 能用:掌握构造、迭代器、增删改等核心接口,理解其与 vector 的接口差异(如无
operator[]); - 明理:理解双向循环链表的底层逻辑,尤其是迭代器失效的场景(仅删除时失效)和反向迭代器的复用设计;
- 会选:根据实际场景在 list 和 vector 之间做选择 —— 频繁增删用 list,频繁访问用 vector。

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



