作为 C++ 初学者,在接触 STL 容器时,最容易混淆的就是vector和list------ 明明都是存数据的容器,为啥用法和场景差这么多?这篇文章就像你的 "list 专属学习笔记",用新手能看懂的语言,把 list 的底层逻辑、核心用法、避技巧讲透。
📚 目录(新手友好版)
1.【唠唠基础】list 是个啥?和 vector 差在哪?
2.【核心技能】list 的 "常用招式"(构造 / 迭代器 / 增删改查)
3.【踩坑预警】迭代器失效?新手最容易栽的坑
4.【动手实操】新手也能看懂的 list 模拟实现
5.【终极对比】list vs vector:啥时候用谁?
6.【复习小结】核心知识点一键回顾
1. 【唠唠基础】list 是个啥?和 vector 差在哪?
对于刚学 C++ 的同学来说,先不用死记 "双向循环链表" 这种专业术语,咱们先打个比方:
- vector就像你手里的一排连续座位,每个座位紧挨着,想找第 5 个座位直接走过去就行(随机访问),但如果要在中间加个座位,就得把后面的人全挪开(插入效率低);
- list就像一串手拉手的小朋友,每个小朋友都记着前面和后面是谁,想在中间加个小朋友,只需要让前后的小朋友换个牵手对象就行(插入效率高),但想找第 5 个小朋友,得从第一个开始数(不支持随机访问)。
1.1 list 的官方定义(简化版)
list 是 STL 里的序列式容器 ,底层是「带头结点的双向循环链表」,核心特点:
✅ 优点:任意位置插入 / 删除元素超快(只改指针,不用搬移数据);插入元素不会让迭代器失效;
❌ 缺点:不能直接访问第 n 个元素(比如list[5]会报错);每个节点要存额外的指针,占内存;
1.2 新手必懂的基础对比(先记结论)

在 C++ 标准库中,std::list 和 std::vector 对于插入操作时的迭代器失效行为不同,根源在于它们的底层存储结构:
vector 内部使用连续内存 (类似动态数组)。插入元素时,若容量不足则重新分配整块内存,并将所有元素移动/拷贝到新空间,此时所有迭代器、引用、指针都会失效;即使容量充足,插入点之后的所有元素也会向后移动,导致指向插入点之后元素的迭代器失效。
list 内部使用双向链表 。插入新元素只需创建新节点并调整相邻节点的前后指针,原有节点的内存地址不变 ,因此所有指向已存在元素的迭代器、引用、指针均保持有效(仅指向被插入元素自身的迭代器是新产生的,但不会影响已有的)。
2. 【核心技能】list 的 "常用招式"(构造 / 迭代器 / 增删改查)
新手学 list,先掌握 "最常用的接口" 就行,不用记所有函数,以下是高频使用的核心操作。
2.1 第一步:创建 list(构造函数)
就像买新本子,先选 "空本子""有 n 页的本子" 还是 "抄别人的本子",list 有 4 种常用创建方式:
cpp
#include <iostream>
#include <list> // 新手必记:用list必须加这个头文件
using namespace std;
int main() {
// 招式1:创建空list(最常用)
list<int> l1;
// 招式2:创建有5个元素,每个元素都是10的list
list<int> l2(5, 10);
// 招式3:拷贝别人的list(复制l2)
list<int> l3(l2);
// 招式4:用数组初始化list(新手也常用)
int arr[] = {1,2,3,4,5};
list<int> l4(arr, arr+5);
// 打印l4看看效果(新手小技巧:用迭代器遍历)
for (auto it = l4.begin(); it != l4.end(); it++) {
cout << *it << " "; // 输出:1 2 3 4 5
}
return 0;
}
2.2 第二步:遍历 list(迭代器的用法)
新手容易懵 "迭代器到底是个啥?"------ 你可以把它理解成 "指向 list 节点的小手",能帮你一个个摸遍 list 里的元素。
list 支持两种迭代器,新手先掌握正向迭代器就行:
cpp
int main() {
list<int> l = {10,20,30,40,50};
// 正向迭代器:从第一个元素走到最后一个
cout << "正向遍历:";
for (list<int>::iterator it = l.begin(); it != l.end(); it++) {
cout << *it << " "; // 输出:10 20 30 40 50
}
// 反向迭代器(新手了解即可):从最后一个走到第一个
cout << "\n反向遍历:";
for (list<int>::reverse_iterator it = l.rbegin(); it != l.rend(); it++) {
cout << *it << " "; // 输出:50 40 30 20 10
}
return 0;
}
💡 小提醒:
- begin()是 "第一个元素的小手",end()是 "最后一个元素的下一个位置"(不是最后一个元素!);
- 反向迭代器的++,其实是往 "前" 走(和正向相反),不用死记,用的时候试一次就懂了。
2.3 第三步:查容量、取元素(高频操作)
list 的容量接口很简单,只有 2 个,元素访问也只有 2 个(因为不能随机访问):
cpp
int main() {
list<int> l = {1,2,3};
// 1. 查容量:empty(是否为空)、size(元素个数)
if (l.empty()) {
cout << "list是空的";
} else {
cout << "list有" << l.size() << "个元素\n"; // 输出:3
}
// 2. 取元素:front(第一个)、back(最后一个)
cout << "第一个元素:" << l.front() << endl; // 输出:1
cout << "最后一个元素:" << l.back() << endl; // 输出:3
// 新手小技巧:front/back返回的是引用,能直接修改元素
l.front() = 100;
cout << "修改后第一个元素:" << l.front() << endl; // 输出:100
return 0;
}
2.4 第四步:增删改查(list 的核心优势)
这是 list 最牛的地方 ------ 任意位置增删都超快,新手先掌握这些高频接口:
cpp
int main() {
list<int> l = {1,2,3};
// 1. 首尾增删(最简单)
l.push_front(0); // 头部加0 → {0,1,2,3}
l.push_back(4); // 尾部加4 → {0,1,2,3,4}
l.pop_front(); // 删头部 → {1,2,3,4}
l.pop_back(); // 删尾部 → {1,2,3}
// 2. 任意位置插入(新手重点)
// 先找到要插入的位置(比如第二个元素)
auto it = l.begin();
++it; // 从第一个元素走到第二个元素的位置
l.insert(it, 99); // 在第二个位置插99 → {1,99,2,3}
// 3. 任意位置删除
it = l.begin();
++it; // 指向99的位置
l.erase(it); // 删除99 → {1,2,3}
// 4. 清空list
// l.clear(); // 清空后list为空,size=0
// 遍历看看最终结果
for (auto num : l) { // 新手简化写法:范围for循环
cout << num << " "; // 输出:1 2 3
}
return 0;
}
💡 小提醒:
- 范围 for 循环(for (auto num : l))是 C++11 的特性,底层还是迭代器,新手用这个遍历更简单;
- 插入 / 删除时,先找迭代器位置,再调用接口,别直接写数字(比如l.insert(2, 99)会报错,因为 list 不支持随机访问)。
3. 【踩坑预警】迭代器失效?新手最容易栽的坑
这是新手学 list 最容易出错的地方!先搞懂 "迭代器失效" 的本质:迭代器指向的节点被删了,这个 "小手" 就抓空了,再用就会崩溃。
cpp
int main() {
list<int> l = {1,2,3,4};
auto it = l.begin();
while (it != l.end()) {
l.erase(it); // 删完it指向的节点,it就失效了
++it; // 报错!失效的迭代器不能++
}
return 0;
}
3.2 正确写法(必记)
有两种写法,推荐第一种(简单好记):
cpp
// 写法1:erase(it++) → 先把it传给erase,再让it自增(此时it还没失效)
int main() {
list<int> l = {1,2,3,4};
auto it = l.begin();
while (it != l.end()) {
l.erase(it++); // 安全!
}
cout << l.size(); // 输出:0
return 0;
}
// 写法2:it = l.erase(it) → erase返回下一个有效迭代器
int main() {
list<int> l = {1,2,3,4};
auto it = l.begin();
while (it != l.end()) {
it = l.erase(it); // 接收返回值,it指向新的有效位置
}
return 0;
}
3.3 必记的迭代器失效规则

4. 【动手实操】新手也能看懂的 list 模拟实现
学完用法,新手可以简单了解 list 的底层实现(不用全写,看懂核心逻辑就行),能帮你更好理解为啥 list 这么用。
4.1 第一步:定义节点结构(小朋友的 "牵手信息")
每个节点要存 "自己的数据""前面的人""后面的人":
cpp
// 模板类:支持存任意类型(int/string等)
template<class T>
struct ListNode {
// 节点构造函数(新手:T()是默认值,比如int就是0)
ListNode(const T& val = T())
: _val(val), _prev(nullptr), _next(nullptr) {}
T _val; // 自己的数据
ListNode* _prev; // 前面的节点
ListNode* _next; // 后面的节点
};
4.2 第二步:封装迭代器(新手的 "小手")
因为 list 的节点不是连续内存,不能直接用指针,所以要封装迭代器,重载++/--/*这些操作:
cpp
template<class T, class Ref, class Ptr>
struct ListIterator {
typedef ListNode<T> Node;
Node* _node; // 指向节点的指针
// 构造:给迭代器绑定一个节点
ListIterator(Node* node) : _node(node) {}
// 解引用:拿到节点里的数据(新手:Ref是引用,能修改)
Ref operator*() { return _node->_val; }
// 迭代器往后走(++)
ListIterator& operator++() {
_node = _node->_next; // 指向后面的节点
return *this;
}
// 迭代器往前⾛(--)
ListIterator& operator--() {
_node = _node->_prev; // 指向前面的节点
return *this;
}
// 判断两个迭代器是否相等
bool operator!=(const ListIterator& it) {
return _node != it._node;
}
};
4.3 第三步:list 主体类(管理整个链表)
核心是 "头结点"(不存数据,只用来统一操作),封装构造、迭代器、增删等接口:
cpp
template<class T>
class List {
typedef ListNode<T> Node;
public:
// 迭代器类型定义(新手:简化写法)
typedef ListIterator<T, T&, T*> iterator;
// 构造空list:创建头结点,自己指向自己(双向循环)
List() {
_head = new Node();
_head->_prev = _head;
_head->_next = _head;
}
// 迭代器接口:返回头结点后面的第一个节点
iterator begin() { return iterator(_head->_next); }
iterator end() { return iterator(_head); } // 尾是头结点
// 尾部插入(新手简化版)
void push_back(const T& val) {
Node* tail = _head->_prev; // 找到最后一个节点
Node* new_node = new Node(val); // 新建节点
// 改指针:尾节点→新节点→头结点
tail->_next = new_node;
new_node->_prev = tail;
new_node->_next = _head;
_head->_prev = new_node;
}
private:
Node* _head; // 头结点
};
4.4 新手测试模拟实现的 list
cpp
int main() {
List<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(3);
// 遍历(和STL的list用法一样!)
for (auto it = l.begin(); it != l.end(); it++) {
cout << *it << " "; // 输出:1 2 3
}
return 0;
}
💡 小总结:模拟实现不用写全,只要知道 "list 是节点拼起来的,迭代器是封装的指针",就能理解为啥 list 不能随机访问、增删快。
5. 【终极对比】list vs vector:啥时候用谁?
新手最纠结 "该用 list 还是 vector",记住这几个场景就行,不用死记原理:

cpp
// 场景1:查成绩(要随机访问)→ 用vector
vector<int> scores = {90,85,95,88};
cout << "第3个同学的成绩:" << scores[2] << endl; // 直接访问
// 场景2:排队买票(频繁有人插队/离开)→ 用list
list<string> queue;
queue.push_back("小明");
queue.push_back("小红");
auto it = queue.begin();
queue.insert(it, "小刚"); // 小刚插队到第一个
6. 【复习小结】核心知识点一键回顾
总结
1.list 底层是双向循环链表 ,核心优势是任意位置 O (1) 增删 ,核心短板是不支持随机访问;
2.迭代器失效是新手最大坑:list 删除后只有被删节点的迭代器失效,推荐用l.erase(it++);
3.选容器的核心原则:查得多用 vector,增删多用 list;
4.模拟实现不用全写,记住 "节点存数据 + 前后指针,迭代器封装指针" 就行;
5.新手用 list 的高频接口:push_back/push_front/insert/erase/begin/end。