C++ STL 详解:list 的使用、迭代器失效、模拟实现与 vector 对比

🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 | 《C语言基础》 | 《数据结构》 | 《机器学习导论》 | 《前端基础》 | 《python基础》 | 《C++从入门到入土》 ✨ 数据即知识,压缩即智能
文章目录
- [C++ STL 详解:list 的使用、迭代器失效、模拟实现与 vector 对比](#C++ STL 详解:list 的使用、迭代器失效、模拟实现与 vector 对比)
-
- 前言
- [一、list 是什么?](#一、list 是什么?)
- [二、list 的底层结构:带头结点的双向循环链表](#二、list 的底层结构:带头结点的双向循环链表)
-
- [1. 双向链表](#1. 双向链表)
- [2. 循环链表](#2. 循环链表)
- [3. 带头结点](#3. 带头结点)
- [三、list 的构造函数](#三、list 的构造函数)
-
- [1. 构造空 list](#1. 构造空 list)
- [2. 构造 n 个相同元素](#2. 构造 n 个相同元素)
- [3. 拷贝构造](#3. 拷贝构造)
- [4. 区间构造](#4. 区间构造)
- [四、list 的遍历方式](#四、list 的遍历方式)
-
- [1. 迭代器遍历](#1. 迭代器遍历)
- [2. 范围 for 遍历](#2. 范围 for 遍历)
- [3. 反向迭代器遍历](#3. 反向迭代器遍历)
- [五、begin、end、rbegin、rend 的关系](#五、begin、end、rbegin、rend 的关系)
- [六、list 的容量接口](#六、list 的容量接口)
-
- [1. empty:判断是否为空](#1. empty:判断是否为空)
- [2. size:返回有效节点个数](#2. size:返回有效节点个数)
- [七、list 的元素访问接口](#七、list 的元素访问接口)
-
- [1. front:访问第一个元素](#1. front:访问第一个元素)
- [2. back:访问最后一个元素](#2. back:访问最后一个元素)
- [八、list 的增删接口](#八、list 的增删接口)
-
- [1. push_front:头插](#1. push_front:头插)
- [2. pop_front:头删](#2. pop_front:头删)
- [3. push_back:尾插](#3. push_back:尾插)
- [4. pop_back:尾删](#4. pop_back:尾删)
- [5. insert:在指定位置前插入](#5. insert:在指定位置前插入)
- [6. erase:删除指定位置元素](#6. erase:删除指定位置元素)
- [7. clear:清空所有有效元素](#7. clear:清空所有有效元素)
- [8. swap:交换两个 list](#8. swap:交换两个 list)
- [九、list 的迭代器失效问题](#九、list 的迭代器失效问题)
- [十、erase 后为什么不能继续使用原迭代器?](#十、erase 后为什么不能继续使用原迭代器?)
- [十一、erase 删除时的正确写法](#十一、erase 删除时的正确写法)
- [十二、删除 list 中所有偶数](#十二、删除 list 中所有偶数)
- [十三、list 为什么不支持随机访问?](#十三、list 为什么不支持随机访问?)
- [十四、list 插入和删除为什么效率高?](#十四、list 插入和删除为什么效率高?)
- [十五、list 的模拟实现思路](#十五、list 的模拟实现思路)
-
- [1. 节点结构](#1. 节点结构)
- [2. list 的基本成员](#2. list 的基本成员)
- [3. push_back 的实现思路](#3. push_back 的实现思路)
- [4. insert 的实现思路](#4. insert 的实现思路)
- [5. erase 的实现思路](#5. erase 的实现思路)
- [十六、list 迭代器的模拟实现思路](#十六、list 迭代器的模拟实现思路)
- 十七、反向迭代器的理解
- [十八、list 和 vector 的对比](#十八、list 和 vector 的对比)
-
- [1. 底层结构](#1. 底层结构)
- [2. 随机访问](#2. 随机访问)
- [3. 插入和删除](#3. 插入和删除)
- [4. 空间利用率和缓存命中](#4. 空间利用率和缓存命中)
- [5. 迭代器失效](#5. 迭代器失效)
- [十九、什么时候用 list,什么时候用 vector?](#十九、什么时候用 list,什么时候用 vector?)
-
- [适合用 vector 的场景](#适合用 vector 的场景)
- [适合用 list 的场景](#适合用 list 的场景)
- [二十、list 常见面试问题](#二十、list 常见面试问题)
-
- [1. list 的底层是什么?](#1. list 的底层是什么?)
- [2. list 支持随机访问吗?](#2. list 支持随机访问吗?)
- [3. list 的 begin 和 end 分别指向哪里?](#3. list 的 begin 和 end 分别指向哪里?)
- [4. list 插入会导致迭代器失效吗?](#4. list 插入会导致迭代器失效吗?)
- [5. list 删除会导致哪些迭代器失效?](#5. list 删除会导致哪些迭代器失效?)
- [6. erase 后为什么要接收返回值?](#6. erase 后为什么要接收返回值?)
- [7. list 和 vector 怎么选?](#7. list 和 vector 怎么选?)
- 总结
前言
在 C++ STL 中,vector 和 list 都是非常重要的序列式容器。
如果说 vector 更像是"可以自动扩容的数组",那么 list 更像是"一串用指针连接起来的节点"。
很多同学刚学 list 的时候,会觉得它没有 vector 好用:不能下标访问,遍历起来还要用迭代器,看起来没有那么直观。
但 list 的优势也很明显:
- 适合频繁插入和删除
- 插入时不会整体搬移元素
- 删除某个节点时,通常只影响被删除节点对应的迭代器
- 底层结构非常适合帮助我们理解链表和 STL 迭代器
所以学习 list 不只是为了会用一个容器,更重要的是通过它理解 STL 容器背后的设计思想。
这篇文章会从使用入手,再逐步讲到底层结构、迭代器失效、模拟实现和 vector 对比。
一、list 是什么?
list 是 C++ STL 中的一个序列式容器,底层通常是双向链表。
使用前需要包含头文件:
cpp
#include <list>
基本使用:
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
for (auto e : lt)
{
cout << e << " ";
}
return 0;
}
输出:
cpp
1 2 3
从表面上看,list 和 vector 都能保存一组数据。
但它们的底层完全不同。
vector 底层是一段连续空间:
text
[1][2][3][4][5]
list 底层是一个个节点通过指针连接起来:
text
[prev | data | next] <-> [prev | data | next] <-> [prev | data | next]
也正因为底层不同,它们的使用场景也不同。
二、list 的底层结构:带头结点的双向循环链表
为了理解 list,最好先理解它的底层结构。
一个典型的 list 可以抽象成:

这里有几个关键词:
1. 双向链表
每个节点不只知道下一个节点,还知道上一个节点。
text
prev <- 当前节点 -> next
所以 list 的迭代器既可以向后走,也可以向前走。
cpp
++it; // 向后移动
--it; // 向前移动
这和单链表不同。单链表只能从前往后走,不能直接往回走。
2. 循环链表
最后一个节点的 next 指向头结点,头结点的 prev 指向最后一个节点。
这样整个链表就形成一个环。
3. 带头结点
头结点一般不存储有效数据,它主要用于统一边界情况。
比如:
- 空链表
- 第一个节点前插入
- 最后一个节点后删除
- begin 和 end 的组织
都可以通过头结点简化处理。

可以简单理解:
text
head 是哨兵节点
真实数据从 head->next 开始
end() 通常对应 head 节点的位置
三、list 的构造函数
list 有几种常见构造方式。
1. 构造空 list
cpp
list<int> lt;
此时链表中没有有效元素。
2. 构造 n 个相同元素
cpp
list<int> lt(5, 10);
表示构造 5 个元素,每个元素都是 10。
内容是:
text
10 10 10 10 10
3. 拷贝构造
cpp
list<int> lt1{1, 2, 3};
list<int> lt2(lt1);
lt2 会拷贝 lt1 中的所有元素。
4. 区间构造
cpp
vector<int> v{1, 2, 3, 4};
list<int> lt(v.begin(), v.end());
这表示用 [first, last) 区间中的元素构造 list。
STL 中很多区间都是左闭右开:
text
[first, last)
也就是包含 first 指向的位置,不包含 last 指向的位置。
四、list 的遍历方式
list 不支持下标访问。
也就是说,不能这样写:
cpp
list<int> lt{1, 2, 3};
cout << lt[0] << endl; // 错误
因为 list 底层不是连续空间,不能像数组一样通过地址偏移直接找到某个元素。
遍历 list 通常有三种方式。
1. 迭代器遍历
cpp
list<int> lt{1, 2, 3, 4};
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
这里可以把迭代器暂时理解成"指向某个节点的对象"。
cpp
*it
表示取出当前节点中的值。
cpp
++it
表示移动到下一个节点。
2. 范围 for 遍历
cpp
list<int> lt{1, 2, 3, 4};
for (auto e : lt)
{
cout << e << " ";
}
如果只是读取元素,范围 for 更简洁。
如果要修改元素,需要使用引用:
cpp
for (auto& e : lt)
{
e *= 2;
}
否则 e 只是元素的拷贝,修改 e 不会影响原链表。
3. 反向迭代器遍历
list 支持反向迭代器:
cpp
list<int> lt{1, 2, 3, 4};
auto rit = lt.rbegin();
while (rit != lt.rend())
{
cout << *rit << " ";
++rit;
}
输出:
cpp
4 3 2 1
注意,反向迭代器的 ++ 是向前移动。
这里需要和正向迭代器区别开来:

五、begin、end、rbegin、rend 的关系
对于 list<int> lt{1, 2, 3, 4}; 来说,可以大致理解成:
text
head <-> 1 <-> 2 <-> 3 <-> 4 <-> head
其中:
text
begin() 指向第一个有效节点,也就是 1
end() 指向最后一个有效节点的下一个位置,通常可以理解为头结点 head
所以:
cpp
auto it = lt.begin();
cout << *it << endl; // 1
但是:
cpp
auto it = lt.end();
cout << *it << endl; // 错误,end() 不能解引用
end() 是边界位置,不是有效元素。
反向迭代器中:
text
rbegin() 从最后一个有效元素开始
rend() 到第一个有效元素之前结束
所以:
cpp
auto rit = lt.rbegin();
cout << *rit << endl; // 4
六、list 的容量接口
list 的容量接口比较简单,常见的是:
cpp
empty()
size()
1. empty:判断是否为空
cpp
list<int> lt;
cout << lt.empty() << endl; // true
如果链表中没有有效元素,返回 true。
2. size:返回有效节点个数
cpp
list<int> lt{1, 2, 3};
cout << lt.size() << endl; // 3
size() 返回当前 list 中有效元素的个数。
注意,list 没有 capacity()。
这是因为 list 不是连续空间,不存在类似 vector 那样"底层容量"的概念。
vector 有:
cpp
size()
capacity()
reserve()
resize()
而 list 只有节点数量,不需要提前扩容。
七、list 的元素访问接口
list 不支持下标访问,但可以访问首尾元素。
1. front:访问第一个元素
cpp
list<int> lt{1, 2, 3};
cout << lt.front() << endl; // 1
front() 返回第一个有效节点中值的引用。
所以也可以修改:
cpp
lt.front() = 100;
2. back:访问最后一个元素
cpp
list<int> lt{1, 2, 3};
cout << lt.back() << endl; // 3
同样,back() 返回的是引用:
cpp
lt.back() = 300;
此时链表变成:
text
1 2 300
需要注意:如果 list 为空,不要调用 front() 和 back(),否则行为是不安全的。
更稳妥的写法是:
cpp
if (!lt.empty())
{
cout << lt.front() << endl;
}
八、list 的增删接口
list 最核心的优势就在插入和删除。
常见接口有:
cpp
push_front()
pop_front()
push_back()
pop_back()
insert()
erase()
clear()
swap()
1. push_front:头插
cpp
list<int> lt{2, 3, 4};
lt.push_front(1);
结果:
text
1 2 3 4
push_front 会在第一个元素前插入新元素。
2. pop_front:头删
cpp
list<int> lt{1, 2, 3};
lt.pop_front();
结果:
text
2 3
3. push_back:尾插
cpp
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
结果:
text
1 2 3
4. pop_back:尾删
cpp
list<int> lt{1, 2, 3};
lt.pop_back();
结果:
text
1 2
5. insert:在指定位置前插入
cpp
list<int> lt{1, 2, 4};
auto it = lt.begin();
++it;
++it; // it 指向 4
lt.insert(it, 3);
结果:
text
1 2 3 4
注意:
cpp
lt.insert(pos, val);
是在 pos 位置之前插入。
6. erase:删除指定位置元素
cpp
list<int> lt{1, 2, 3, 4};
auto it = lt.begin();
++it; // it 指向 2
lt.erase(it);
结果:
text
1 3 4
erase 会删除迭代器指向的节点。
删除后,被删除节点对应的迭代器会失效。
7. clear:清空所有有效元素
cpp
list<int> lt{1, 2, 3};
lt.clear();
cout << lt.size() << endl; // 0
clear() 会清空所有有效节点,但链表对象本身还在。
8. swap:交换两个 list
cpp
list<int> lt1{1, 2, 3};
list<int> lt2{10, 20};
lt1.swap(lt2);
交换后:
text
lt1: 10 20
lt2: 1 2 3
九、list 的迭代器失效问题
迭代器失效是 STL 容器中非常重要的一类问题。
可以先简单理解:
迭代器失效,就是迭代器原来指向的节点或空间已经不能继续安全使用了。
对于 vector 来说,插入元素可能触发扩容,扩容会导致底层空间变化,所以很多迭代器都会失效。
但 list 不一样。
list 是一个个独立节点连接起来的链表。
插入新节点时,只需要调整前后节点的指针关系,不需要整体搬移已有节点。

所以:
text
list 插入元素通常不会导致已有迭代器失效
list 删除元素时,只有指向被删除节点的迭代器失效
其他迭代器不受影响
这一点是 list 相比 vector 的重要优势。
十、erase 后为什么不能继续使用原迭代器?
看下面这段代码:
cpp
list<int> lt{1, 2, 3, 4};
auto it = lt.begin();
while (it != lt.end())
{
lt.erase(it);
++it;
}
这段代码是错误的。
问题出在:
cpp
lt.erase(it);
执行完之后,it 指向的节点已经被删除了。
此时 it 已经失效。

后面再写:
cpp
++it;
就是对一个失效迭代器进行操作,可能导致程序崩溃或者出现不可预期结果。
十一、erase 删除时的正确写法
更稳妥的写法是使用 erase 的返回值。
cpp
list<int> lt{1, 2, 3, 4};
auto it = lt.begin();
while (it != lt.end())
{
it = lt.erase(it);
}
为什么这样写?
因为 erase 删除当前节点后,会返回被删除节点后面那个节点的迭代器。
比如:
text
1 <-> 2 <-> 3 <-> 4
↑
it 指向 2
执行:
cpp
it = lt.erase(it);
删除 2 后:
text
1 <-> 3 <-> 4
↑
it 指向 3
这样循环就可以继续安全进行。
也可以看到,有些代码会写成:
cpp
lt.erase(it++);
这个写法的含义是:先把旧的 it 传给 erase,同时让 it 移动到下一个位置。
不过初学阶段,我更建议写成:
cpp
it = lt.erase(it);
它的意图最清楚,也最不容易误解。
十二、删除 list 中所有偶数
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt{1, 2, 3, 4, 5, 6};
auto it = lt.begin();
while (it != lt.end())
{
if (*it % 2 == 0)
{
it = lt.erase(it);
}
else
{
++it;
}
}
for (auto e : lt)
{
cout << e << " ";
}
return 0;
}
输出:
text
1 3 5
这里的关键是:
cpp
it = lt.erase(it);
如果当前元素要删除,就让 it 接收删除后的下一个位置。
如果当前元素不删除,才手动执行:
cpp
++it;
十三、list 为什么不支持随机访问?
vector 可以这样访问:
cpp
vector<int> v{1, 2, 3};
cout << v[1] << endl;
因为 vector 底层是连续空间。
假设第一个元素地址是 _start,那么第 i 个元素可以直接通过地址偏移找到:
text
_start + i
所以访问 v[i] 是 O(1)。
但 list 不一样。
list 的节点在内存中不一定连续。
它们可能是这样分布的:
text
node1 在地址 A
node2 在地址 C
node3 在地址 B
node4 在地址 F
虽然逻辑上是:
text
node1 <-> node2 <-> node3 <-> node4
但物理内存中并不连续。
所以要访问第 4 个元素,只能从头开始一个节点一个节点走过去。
text
1 -> 2 -> 3 -> 4
因此访问某个位置的复杂度是 O(N)。
这就是为什么 list 类中没有重载 operator[]。

十四、list 插入和删除为什么效率高?
假设现在有链表:
text
1 <-> 2 <-> 4
现在要在 2 和 4 之间插入 3。
只需要创建一个新节点,然后修改前后几个指针:
text
1 <-> 2 <-> 3 <-> 4
不需要像 vector 那样把后面的元素整体往后搬。
对于 list 来说,只要已经拿到了插入位置的迭代器,插入和删除节点本身通常就是 O(1)。
但这句话有一个前提:
已经找到了位置。
如果你要先从头遍历找到某个位置,这个查找过程仍然是 O(N)。
所以不能简单说:
text
list 所有操作都是 O(1)
更准确地说:
text
list 在已知位置处插入和删除节点效率高
list 查找某个位置或某个值仍然需要遍历
十五、list 的模拟实现思路
如果要自己模拟实现一个简化版 list,一般需要先定义节点。
1. 节点结构
cpp
template<class T>
struct ListNode
{
ListNode<T>* _prev;
ListNode<T>* _next;
T _data;
ListNode(const T& data = T())
: _prev(nullptr)
, _next(nullptr)
, _data(data)
{}
};
每个节点包含三部分:
text
_prev:指向前一个节点
_next:指向后一个节点
_data:存储数据
2. list 的基本成员
cpp
template<class T>
class list
{
typedef ListNode<T> Node;
public:
list()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
private:
Node* _head;
};
这里 _head 是头结点。
空链表时:
text
_head->_next == _head
_head->_prev == _head
这就是带头双向循环链表的典型结构。
3. push_back 的实现思路
尾插可以复用 insert。
尾插本质上是在 end() 位置之前插入。
因为 end() 对应头结点,所以:
cpp
push_back(x)
等价于:
cpp
insert(end(), x)
简化理解:
cpp
void push_back(const T& x)
{
insert(end(), x);
}
4. insert 的实现思路
假设要在 pos 位置前插入新节点。
原来的连接关系:
text
prev <-> pos
插入后:
text
prev <-> newnode <-> pos
核心逻辑就是修改四条指针:
cpp
newnode->_next = pos;
newnode->_prev = prev;
prev->_next = newnode;
pos->_prev = newnode;
这也是链表插入的本质。
5. erase 的实现思路
删除某个节点时,假设要删除 pos:
text
prev <-> pos <-> next
删除后:
text
prev <-> next
核心逻辑:
cpp
prev->_next = next;
next->_prev = prev;
delete pos;
同时,最好返回 next 位置的迭代器,方便外部继续遍历:
cpp
iterator erase(iterator pos)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
这也解释了为什么标准库里的 erase 返回下一个位置。
十六、list 迭代器的模拟实现思路
list 的迭代器不能简单地用原生指针 T*。
因为 list 的节点不是连续空间。
vector 的迭代器可以接近原生指针,因为它底层连续。
但 list 的迭代器需要封装节点指针。
大致结构可以这样写:
cpp
template<class T>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T> Self;
Node* _node;
ListIterator(Node* node)
: _node(node)
{}
T& operator*()
{
return _node->_data;
}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
bool operator!=(const Self& it) const
{
return _node != it._node;
}
bool operator==(const Self& it) const
{
return _node == it._node;
}
};
这里的关键是:
cpp
operator++()
不是地址加 1,而是走到当前节点的下一个节点:
cpp
_node = _node->_next;
这就是 list 迭代器和 vector 迭代器最大的不同之一。
十七、反向迭代器的理解
反向迭代器看起来特殊,其实可以基于正向迭代器进行封装。
核心思想是:
text
反向迭代器的 ++ 等价于正向迭代器的 --
反向迭代器的 -- 等价于正向迭代器的 ++
比如正向遍历:
text
1 -> 2 -> 3 -> 4
反向遍历:
text
4 -> 3 -> 2 -> 1
所以反向迭代器内部可以保存一个正向迭代器 _it。
当反向迭代器执行:
cpp
++rit;
内部其实可以让正向迭代器:
cpp
--_it;
这就是"适配器"的思想:
不重新造一套完整逻辑,而是包装已有能力,让它表现出新的行为。
十八、list 和 vector 的对比
list 和 vector 都是序列式容器,但它们的底层结构不同,所以适用场景也不同。
1. 底层结构
vector:
text
动态顺序表
底层是一段连续空间
list:
text
带头结点的双向循环链表
节点之间通过指针连接
2. 随机访问
vector 支持随机访问:
cpp
v[i]
时间复杂度是 O(1)。
list 不支持随机访问,访问某个位置需要从头或尾遍历,时间复杂度是 O(N)。
3. 插入和删除
vector 在中间插入和删除时,通常需要搬移元素。
例如:
text
[1][2][4][5]
在 2 和 4 之间插入 3:
text
[1][2][3][4][5]
后面的 4 和 5 都要往后移动。
如果空间不够,还可能触发扩容,代价更高。
list 在已知位置插入和删除时,只需要修改指针,不需要搬移其他节点。
所以在大量中间插入和删除的场景中,list 更合适。
4. 空间利用率和缓存命中
vector 底层连续,空间利用率较高,缓存友好。
CPU 读取连续内存时通常更高效,所以遍历 vector 往往很快。
list 的每个节点通常单独申请空间。
每个节点除了数据,还要存储前后指针。
text
prev + data + next
因此额外空间开销更大,而且节点分散在内存中,缓存利用率通常不如 vector。
这也是一个很重要的事实:
list 插入删除理论上很强,但并不代表所有场景都比 vector 快。
在实际开发中,如果数据量不是特别大,或者主要是遍历和随机访问,vector 往往表现更好。因此在实际工程中也有链表已死的说法。

5. 迭代器失效
vector:
text
插入可能导致扩容,扩容后旧迭代器全部失效
删除元素后,被删位置及其后的迭代器可能失效
list:
text
插入不会导致其他迭代器失效
删除只会导致被删除节点对应的迭代器失效
其他迭代器通常不受影响
十九、什么时候用 list,什么时候用 vector?
可以简单这样判断。
适合用 vector 的场景
如果你需要:
text
高效随机访问
频繁通过下标访问元素
主要在尾部插入数据
希望遍历速度快
希望内存连续、缓存友好
优先考虑 vector。
例如:
cpp
vector<int> scores;
适合保存成绩、数组、动态表、图像像素数据、算法题中的一维数组等。
适合用 list 的场景
如果你需要:
text
频繁在中间插入和删除
已经能快速拿到插入或删除位置的迭代器
不关心随机访问
希望插入删除时尽量不影响其他迭代器
可以考虑 list。
例如:
cpp
list<int> tasks;
适合某些任务队列、需要稳定迭代器的结构、频繁节点调整的场景。
不过要注意:
如果你每次插入前都要从头遍历查找位置,那么查找本身仍然是 O(N)。
这时候未必一定比 vector 更快。
二十、list 常见面试问题
1. list 的底层是什么?
通常是带头结点的双向循环链表。
2. list 支持随机访问吗?
不支持。
因为节点不连续,不能通过地址偏移直接访问第 n 个元素。
3. list 的 begin 和 end 分别指向哪里?
begin() 指向第一个有效节点。
end() 指向最后一个有效节点的下一个位置,通常可以理解为头结点位置。
end() 不能解引用。
4. list 插入会导致迭代器失效吗?
通常不会导致已有迭代器失效。
因为插入只是新增节点并调整指针,不需要搬移已有节点。
5. list 删除会导致哪些迭代器失效?
删除会导致指向被删除节点的迭代器失效。
其他节点的迭代器通常不受影响。
6. erase 后为什么要接收返回值?
因为当前节点被删除后,原迭代器失效。
erase 返回下一个有效位置,所以遍历删除时应该写:
cpp
it = lt.erase(it);
7. list 和 vector 怎么选?
需要随机访问、连续存储、主要尾插:优先 vector。
需要频繁中间插入删除,并且不关心随机访问:可以考虑 list。
总结
list 是 STL 中非常典型的链式结构容器。
它不像 vector 那样支持随机访问,也没有连续空间带来的缓存优势,但它在插入删除方面有自己的特点。
不过在真实开发中,不要只看时间复杂度,也要考虑缓存命中、空间开销、代码可读性和数据规模。