目录
[一、list 基础介绍](#一、list 基础介绍)
[1.1 什么是 list?](#1.1 什么是 list?)
[1.2 list 的结构特性](#1.2 list 的结构特性)
[二、list 核心接口使用:从构造到元素操作](#二、list 核心接口使用:从构造到元素操作)
[2.1 list 的构造函数:初始化链表](#2.1 list 的构造函数:初始化链表)
[2.2 list 迭代器:遍历链表的 "钥匙"](#2.2 list 迭代器:遍历链表的 “钥匙”)
[2.3 list 容量接口:判断链表状态](#2.3 list 容量接口:判断链表状态)
[2.4 list 元素访问接口:获取头尾元素](#2.4 list 元素访问接口:获取头尾元素)
[2.5 list 元素修改接口:插入、删除与清空](#2.5 list 元素修改接口:插入、删除与清空)
[2.6 list 迭代器失效问题:删除操作的 "陷阱"](#2.6 list 迭代器失效问题:删除操作的 “陷阱”)
前言
在 C++ 标准模板库(STL)中,序列式容器是我们日常开发中频繁使用的工具,而
list
作为其中极具特色的一员,凭借其独特的底层结构,在处理频繁插入、删除操作的场景中展现出显著优势。本文将从list
的基础介绍与使用入手,深入剖析其底层原理,帮助大家全面掌握list
的特性与适用场景,为实际开发中的容器选择提供清晰指引。下面就让我们正式开始吧!
一、list 基础介绍
1.1 什么是 list?
list
是 C++ STL 中的序列式容器,其底层实现是基于带头结点的环状双向链表(也称为双向循环链表)。这种结构意味着链表中的每个节点都包含三个核心部分:
- 存储数据的区域(
data
) - 指向前驱节点的指针(
prev
) - 指向后继节点的指针(
next
)
同时,链表还存在一个头结点 (又叫做哨兵节点),该节点不存储有效数据,仅用于简化链表的插入、删除操作逻辑 ------ 无需额外判断链表是否为空或操作位置是否为头尾,统一了所有位置的操作接口。
从上图的示例 **list<int> ilist;**可以看出,list
是一个模板类,支持存储任意数据类型(如 int
、string
、自定义结构体等),因此我们要在使用时需指定具体的数据类型,这体现了 STL 的泛型特性。
1.2 list 的结构特性
为了更直观地理解 list
的结构,我们结合上面的示意图进行拆解分析:
- 正向遍历 :从 ilist.begin() 开始(指向第一个有效数据节点),通过节点的
next
指针依次访问后续节点,直到 ilist.end()(指向头结点,即最后一个有效节点的下一个位置)。 - 逆向遍历 :从 ilist.rbegin()开始(指向最后一个有效数据节点,对应正向迭代器的end() 位置),通过节点的
prev
指针依次访问前驱节点,直到 ilist.rend()(指向头结点,对应正向迭代器的 begin() 位置)。 - 环状特性 :头结点的
prev
指针指向最后一个有效节点,最后一个有效节点的next
指针指向头结点,形成一个闭环,确保遍历过程中不会出现 "越界" 问题。
例如,如果 ilist
中存储的数据为 [0, 1, 2, 3, 4],那么其结构可表示为:
cpp
头结点 <-> 0(begin()) <-> 1 <-> 2 <-> 3 <-> 4 <-> 头结点(end())
当我们使用 find(ilist.begin(), ilist.end(), 3) 查找数据 3
时,函数会从 begin() 开始遍历,最终返回指向数据 3
所在节点的迭代器。
二、list 核心接口使用:从构造到元素操作
list
提供了丰富的接口用于完成容器的初始化、遍历、容量判断、元素访问与修改等操作。掌握这些接口的正确用法是高效使用 list
的基础,以下将按功能分类详解关键接口,并结合代码示例说明。
2.1 list 的构造函数:初始化链表
list
提供了 4 种常用的构造方式,适用于不同的初始化场景,具体接口说明如下表所示:
构造函数原型 | 接口说明 | 适用场景 |
---|---|---|
list (size_type n, const value_type& val = value_type()) | 构造一个包含 n 个值为 val 的元素的 list |
需要初始化固定数量、相同值的链表(如初始化 5 个 0) |
list() | 构造空的 list |
初始时链表为空,后续通过插入操作添加元素 |
list (const list& x) | 拷贝构造函数,用已有的 list x 构造新链表 |
需要创建一个与已有链表完全相同的副本 |
list (InputIterator first, InputIterator last) | 用区间 [first, last) 中的元素构造 list (first 和 last 为迭代器) |
从其他容器(如数组、vector )中获取元素初始化链表 |
list 构造函数的使用示例如下:
cpp
#include <list>
#include <iostream>
using namespace std;
int main() {
// 1. 构造包含 5 个 10 的 list
list<int> l1(5, 10);
cout << "l1: ";
for (auto it = l1.begin(); it != l1.end(); ++it) {
cout << *it << " "; // 输出:10 10 10 10 10
}
cout << endl;
// 2. 构造空 list
list<int> l2;
cout << "l2 是否为空:" << (l2.empty() ? "是" : "否") << endl; // 输出:是
// 3. 拷贝构造(用 l1 构造 l3)
list<int> l3(l1);
cout << "l3: ";
for (auto it = l3.begin(); it != l3.end(); ++it) {
cout << *it << " "; // 输出:10 10 10 10 10
}
cout << endl;
// 4. 用数组区间构造
int arr[] = {1, 2, 3, 4, 5};
list<int> l4(arr, arr + 5); // 区间 [arr, arr+5) 包含 arr[0] 到 arr[4]
cout << "l4: ";
for (auto it = l4.begin(); it != l4.end(); ++it) {
cout << *it << " "; // 输出:1 2 3 4 5
}
cout << endl;
return 0;
}
2.2 list 迭代器:遍历链表的 "钥匙"
迭代器是连接容器与算法的桥梁,对于 list
而言,迭代器可以暂时理解为 "指向节点的指针",通过迭代器可以访问节点数据、移动到相邻节点。list
提供了正向迭代器和反向迭代器,满足不同遍历需求。
迭代器的接口说明如下:
函数声明 | 接口说明 | 遍历方向 |
---|---|---|
begin() | 返回指向第一个有效元素的正向迭代器 | 从前往后(++ 移动到下一个元素) |
end() | 返回指向最后一个有效元素的下一个位置(头结点)的正向迭代器 | 作为正向遍历的结束条件 |
rbegin() | 返回指向最后一个有效元素 的反向迭代器(对应正向迭代器的 end() 位置) |
从后往前(++ 移动到前一个元素) |
rend() | 返回指向第一个有效元素的前一个位置 (头结点)的反向迭代器(对应正向迭代器的 begin() 位置) |
作为反向遍历的结束条件 |

在使用迭代器时,我们需要注意以下事项:
- 正向迭代器操作 :对 begin() 返回的迭代器执行
++
操作,会移动到下一个有效元素;执行*
操作,会获取当前节点的元素值。 - 反向迭代器操作 :对 rbegin() 返回的迭代器执行
++
操作,会移动到前一个有效元素(本质是正向迭代器的--
操作);执行*
操作,同样获取当前节点的元素值。 - 迭代器范围 :STL 中所有迭代器相关的算法(如
find
、for_each
)均遵循 "左闭右开" 原则,即范围[first, last)
包含first
指向的元素,不包含last
指向的元素。
list 迭代器的使用示例如下所示:
cpp
#include <list>
#include <iostream>
#include <algorithm> // 包含 find 函数
using namespace std;
int main() {
list<int> l = {1, 2, 3, 4, 5};
// 1. 正向遍历:begin() + end()
cout << "正向遍历 l: ";
for (auto it = l.begin(); it != l.end(); ++it) {
cout << *it << " "; // 输出:1 2 3 4 5
}
cout << endl;
// 2. 反向遍历:rbegin() + rend()
cout << "反向遍历 l: ";
for (auto rit = l.rbegin(); rit != l.rend(); ++rit) {
cout << *rit << " "; // 输出:5 4 3 2 1
}
cout << endl;
// 3. 用迭代器查找元素(find 函数)
auto it = find(l.begin(), l.end(), 3);
if (it != l.end()) {
cout << "找到元素 3,其下一个元素为:" << *(++it) << endl; // 输出:4
} else {
cout << "未找到元素 3" << endl;
}
return 0;
}
2.3 list 容量接口:判断链表状态
list
提供了 2 个核心容量接口,用于判断链表是否为空和获取有效元素个数,接口说明如下:
函数声明 | 接口说明 | 返回值类型 |
---|---|---|
empty() | 检测 list 是否为空(有效元素个数为 0) |
bool (空返回 true ,非空返回 false ) |
size() | 返回 list 中有效元素的个数 |
size_type (无符号整数类型,通常为 unsigned int ) |
list 容量接口的使用示例如下:
cpp
#include <list>
#include <iostream>
using namespace std;
int main() {
list<int> l;
// 初始为空
cout << "初始时,l 是否为空:" << (l.empty() ? "是" : "否") << endl; // 输出:是
cout << "初始时,l 的大小:" << l.size() << endl; // 输出:0
// 插入元素后
l.push_back(1);
l.push_back(2);
l.push_back(3);
cout << "插入 3 个元素后,l 是否为空:" << (l.empty() ? "是" : "否") << endl; // 输出:否
cout << "插入 3 个元素后,l 的大小:" << l.size() << endl; // 输出:3
// 清空元素后
l.clear();
cout << "清空后,l 是否为空:" << (l.empty() ? "是" : "否") << endl; // 输出:是
cout << "清空后,l 的大小:" << l.size() << endl; // 输出:0
return 0;
}
2.4 list 元素访问接口:获取头尾元素
由于 list
底层是链表结构,不支持随机访问(如 vector
的 []
运算符),因此仅提供了获取第一个元素 和最后一个元素的接口,避免用户通过下标访问导致效率低下(链表下标访问需遍历整个链表,时间复杂度为 O (N))。
函数声明 | 接口说明 | 返回值类型 |
---|---|---|
front() | 返回 list 第一个有效节点中元素的引用 |
value_type& (可修改元素值) |
back() | 返回 list 最后一个有效节点中元素的引用 |
value_type& (可修改元素值) |
list 元素访问接口的使用示例如下:
cpp
#include <list>
#include <iostream>
using namespace std;
int main() {
list<int> l = {10, 20, 30, 40};
// 获取头尾元素
cout << "第一个元素:" << l.front() << endl; // 输出:10
cout << "最后一个元素:" << l.back() << endl; // 输出:40
// 修改头尾元素(通过引用)
l.front() = 100;
l.back() = 400;
cout << "修改后,第一个元素:" << l.front() << endl; // 输出:100
cout << "修改后,最后一个元素:" << l.back() << endl; // 输出:400
// 遍历验证
cout << "修改后的链表:";
for (auto it = l.begin(); it != l.end(); ++it) {
cout << *it << " "; // 输出:100 20 30 400
}
cout << endl;
return 0;
}
2.5 list 元素修改接口:插入、删除与清空
函数声明 | 接口说明 | 时间复杂度 |
---|---|---|
push_front(const value_type& val) | 在链表头部 插入值为 val 的元素 |
O(1) |
pop_front() | 删除链表头部的第一个有效元素 | O (1)(需注意链表不能为空) |
push_back(const value_type& val) | 在链表尾部 插入值为 val 的元素 |
O(1) |
pop_back() | 删除链表尾部的最后一个有效元素 | O (1)(需注意链表不能为空) |
insert(iterator pos, const value_type& val) | 在迭代器 pos 指向的位置之前 插入值为 val 的元素 |
O (1)(需先通过迭代器找到 pos 位置,查找过程为 O (N)) |
erase(iterator pos) | 删除迭代器 pos 指向的元素 |
O (1)(同理,查找 pos 位置为 O (N)) |
swap(list& x) | 交换当前链表与 x 中的所有元素 |
O (1)(仅交换链表的头指针等核心成员,不交换元素本身) |
clear() | 清空链表中的所有有效元素(头结点保留) | O (N)(需遍历所有节点释放内存) |
list 元素修改接口的使用示例如下所示:
cpp
#include <list>
#include <iostream>
using namespace std;
int main() {
list<int> l;
// 1. 头部/尾部插入(push_front / push_back)
l.push_back(20); // 链表:[20]
l.push_front(10); // 链表:[10, 20]
l.push_back(30); // 链表:[10, 20, 30]
cout << "插入后链表:";
for (auto it = l.begin(); it != l.end(); ++it) {
cout << *it << " "; // 输出:10 20 30
}
cout << endl;
// 2. 头部/尾部删除(pop_front / pop_back)
l.pop_front(); // 链表:[20, 30]
l.pop_back(); // 链表:[20]
cout << "删除后链表:";
for (auto it = l.begin(); it != l.end(); ++it) {
cout << *it << " "; // 输出:20
}
cout << endl;
// 3. 任意位置插入(insert)
auto it = l.begin(); // it 指向 20
l.insert(it, 15); // 在 20 之前插入 15,链表:[15, 20]
cout << "insert 后链表:";
for (auto it = l.begin(); it != l.end(); ++it) {
cout << *it << " "; // 输出:15 20
}
cout << endl;
// 4. 任意位置删除(erase)
it = l.begin(); // it 指向 15
l.erase(it); // 删除 15,链表:[20]
cout << "erase 后链表:";
for (auto it = l.begin(); it != l.end(); ++it) {
cout << *it << " "; // 输出:20
}
cout << endl;
// 5. 交换链表(swap)
list<int> l2 = {100, 200, 300};
cout << "交换前 l2:";
for (auto it = l2.begin(); it != l2.end(); ++it) {
cout << *it << " "; // 输出:100 200 300
}
cout << endl;
l.swap(l2);
cout << "交换后 l:";
for (auto it = l.begin(); it != l.end(); ++it) {
cout << *it << " "; // 输出:100 200 300
}
cout << endl;
cout << "交换后 l2:";
for (auto it = l2.begin(); it != l2.end(); ++it) {
cout << *it << " "; // 输出:20
}
cout << endl;
// 6. 清空链表(clear)
l.clear();
cout << "clear 后 l 是否为空:" << (l.empty() ? "是" : "否") << endl; // 输出:是
return 0;
}
2.6 list 迭代器失效问题:删除操作的 "陷阱"
迭代器失效是使用 list
时最容易踩的 "坑" 之一。所谓迭代器失效,是指迭代器指向的节点已被删除,此时再使用该迭代器(如 ++
、*
)会导致未定义行为(程序崩溃、结果异常等)。
由于 list
底层是双向循环链表,因此其迭代器失效是具有明确的规律的:
- 插入操作 :不会导致任何迭代器失效。插入新节点时,仅修改相邻节点的
prev
和next
指针,原有节点的地址不变,因此指向原有节点的迭代器依然有效。 - 删除操作 :仅导致指向被删除节点的迭代器失效,其他迭代器(指向未删除节点的)不受影响。因为删除节点后,该节点的内存被释放,迭代器指向的地址变为 "野地址",无法再使用。
下面给大家看一个迭代器失效导致崩溃的错误示例:
cpp
#include <list>
#include <iostream>
using namespace std;
void TestInvalidIterator() {
int arr[] = {1, 2, 3, 4, 5};
list<int> l(arr, arr + 5);
auto it = l.begin();
// 错误写法:erase 后 it 已失效,再执行 ++it 会导致未定义行为
while (it != l.end()) {
l.erase(it); // erase 后,it 指向的节点被删除,it 失效
++it; // 对失效的迭代器执行 ++,程序可能崩溃
}
}
int main() {
TestInvalidIterator(); // 运行时可能崩溃
return 0;
}
正确写法应该是:使用erase 返回值重置迭代器
list
的 erase
函数有一个重要特性:删除节点后,会返回指向被删除节点的下一个节点的迭代器 。因此,我们可以通过 it = l.erase(it) 的方式重置迭代器,避免失效问题。此外,也可以通过 l.erase(it++) 实现(先将 it
传入 erase
,再执行 it++
,此时传入的是删除前的 it
,it
自身已指向 next 节点)。如下所示:
cpp
#include <list>
#include <iostream>
using namespace std;
void TestValidIterator() {
int arr[] = {1, 2, 3, 4, 5};
list<int> l(arr, arr + 5);
auto it = l.begin();
// 正确写法 1:用 erase 的返回值重置迭代器
while (it != l.end()) {
it = l.erase(it); // erase 返回下一个节点的迭代器,重置 it
}
cout << "正确写法 1 后,l 是否为空:" << (l.empty() ? "是" : "否") << endl; // 输出:是
// 重新初始化链表,测试正确写法 2
l.assign(arr, arr + 5);
it = l.begin();
while (it != l.end()) {
l.erase(it++); // 先传 it 给 erase,再 it++(指向 next 节点)
}
cout << "正确写法 2 后,l 是否为空:" << (l.empty() ? "是" : "否") << endl; // 输出:是
}
int main() {
TestValidIterator(); // 正常运行,无崩溃
return 0;
}
总结
本期博客为大家介绍了list容器的基础概念、底层原理以及核心接口的使用,希望能够对大家有所帮助!下期博客我将为大家介绍list容器的模拟实现,请大家多多关注!