
前言
噔噔哒!天空一声巨响,小小风闪亮登场!大家好呀,今天是【从零开始学 C++】专题的第十一篇,我们来学习 STL 中另一个非常重要的容器 ------list。
如果你已经学过 vector,那今天的内容你会觉得既熟悉又陌生。熟悉是因为它们都是容器,接口长得很像;陌生是因为它们的底层实现完全不同,一个是数组,一个是链表。
💡 面试高频考点:vector 和 list 的区别,这是面试必考题!今天我会用大白话给你讲透,保证看完就能答上来。
第一部分:list 的基本使用
(一)list 的介绍和特点
简介作用
list 就是双向链表!
还记得数据结构里的链表吗?每个节点存着数据和两个指针,一个指向前一个节点,一个指向后一个节点。STL 里的 list 就是用带头双向循环链表实现的。
和 vector 对比:
|--------|----------------------|------------------|
| 特性 | vector(数组) | list(链表) |
| 底层 | 连续的数组空间 | 不连续的节点,靠指针连接 |
| 随机访问 | ✅ 支持 \[\] 直接访问,O (1) | ❌ 不支持,必须遍历,O (n) |
| 中间插入删除 | ❌ 要移动元素,O (n) | ✅ 只改指针,O (1) |
| 空间浪费 | 会预留空间,可能浪费 | 每个节点多两个指针开销 |
大白话总结:
-
vector 像高铁:座位是连续的,找第 10 排直接走过去(快),但中间插个人要让所有人挪位置(慢)
-
list 像珍珠项链:珠子不连续,靠线连,找第 10 颗得一颗颗数(慢),但中间插颗珠子只需要改两根线(快)
什么时候用 list?
-
需要频繁在头部或中间插入删除元素
-
不需要随机访问元素
-
元素较大,拷贝代价高
(二)list 的构造函数
简介作用
list 的构造函数和 vector 基本一样,毕竟都是 STL 容器,设计风格统一。
代码示例
cpp
#include <iostream>
#include <list>
using namespace std;
int main() {
// 示例1:无参构造 - 创建空的list
list<int> l1;
cout << "l1的大小:" << l1.size() << endl; // 0
// 示例2:n个元素构造 - 创建包含5个元素的list,每个都是默认值
list<int> l2(5); // 5个0
// 示例3:n个指定值构造 - 创建包含5个10的list
list<int> l3(5, 10); // 5个10
// 示例4:迭代器区间构造 - 用另一个容器的区间初始化
int arr[] = {1, 2, 3, 4, 5};
list<int> l4(arr, arr + 5); // 用数组初始化
// 示例5:拷贝构造 - 用另一个list拷贝
list<int> l5(l4); // 和l4一模一样
return 0;
}
🐛 经典 Bug 和坑点
坑点 1:注意区分构造和赋值
cpp
list<int> l(5, 10); // 正确:构造5个10
// list<int> l(); // 错误!这是函数声明,不是构造空对象
list<int> l; // 正确:构造空对象
(三)list 的迭代器
简介作用
重点来了!list 的迭代器不是原生指针 ,这是和 vector 最大的区别之一!
-
vector 的迭代器本质就是原生指针,因为底层是连续数组
-
list 的迭代器是一个封装了节点指针的类,因为节点不连续,不能直接 ++--
迭代器类型:
-
正向迭代器:
iterator -
const 正向迭代器:
const_iterator(只读) -
反向迭代器:
reverse_iterator -
const 反向迭代器:
const_reverse_iterator
代码示例
cpp
#include <iostream>
#include <list>
using namespace std;
int main() {
list<int> l;
for (int i = 1; i <= 5; i++) {
l.push_back(i);
}
// 示例1:正向迭代器遍历
cout << "正向遍历:";
list<int>::iterator it = l.begin();
while (it != l.end()) {
cout << *it << " ";
++it; // 可以++,但不能it += 2!
}
cout << endl;
// 示例2:const迭代器(只读)
cout << "const迭代器:";
list<int>::const_iterator cit = l.cbegin();
while (cit != l.cend()) {
cout << *cit << " ";
// *cit = 10; // 错误!const迭代器不能修改
++cit;
}
cout << endl;
// 示例3:反向迭代器
cout << "反向遍历:";
list<int>::reverse_iterator rit = l.rbegin();
while (rit != l.rend()) {
cout << *rit << " ";
++rit;
}
cout << endl;
return 0;
}
🐛 经典 Bug 和坑点
坑点 1:list 迭代器不支持随机访问!
cpp
list<int>::iterator it = l.begin();
++it; // ✅ 支持
--it; // ✅ 支持
// it += 3; // ❌ 不支持!链表不能跳
// it[2]; // ❌ 不支持!
坑点 2:迭代器类型不匹配
cpp
// const对象只能用const迭代器
const list<int> cl(l);
// list<int>::iterator it = cl.begin(); // ❌ 错误!
list<int>::const_iterator it = cl.begin(); // ✅ 正确
💡 面试考点:为什么 list 迭代器不是原生指针? 因为链表节点在内存中不连续,原生指针 ++ 会跳到下一个字节而不是下一个节点!必须封装成类,重载 ++ 运算符让它指向下一个节点。
(四)list 的容量和元素访问
简介作用
list 没有 capacity 和 reserve,因为链表不需要预分配空间!
常用接口:
-
size():返回元素个数 -
empty():判断是否为空 -
front():返回第一个元素的引用 -
back():返回最后一个元素的引用
代码示例
cpp
#include <iostream>
#include <list>
using namespace std;
int main() {
list<int> l;
cout << "是否为空:" << (l.empty() ? "是" : "否") << endl; // 是
cout << "元素个数:" << l.size() << endl; // 0
for (int i = 1; i <= 5; i++) {
l.push_back(i);
}
cout << "是否为空:" << (l.empty() ? "是" : "否") << endl; // 否
cout << "元素个数:" << l.size() << endl; // 5
// 访问首尾元素
cout << "第一个元素:" << l.front() << endl; // 1
cout << "最后一个元素:" << l.back() << endl; // 5
// 可以修改
l.front() = 100;
l.back() = 200;
cout << "修改后首元素:" << l.front() << endl; // 100
cout << "修改后尾元素:" << l.back() << endl; // 200
return 0;
}
⚠️ 注意:list不支持 \[\] 随机访问,也没有 at () 函数!要访问中间元素只能遍历。
(五)list 的增删查改接口
1. push_back/pop_back、push_front/pop_front
简介作用
这是 list 的强项!头尾插入删除都是 O (1),比 vector 快多了。
vector 的 push_front 要移动所有元素,O (n),而 list 只需要改指针。
代码示例
cpp
#include <iostream>
#include <list>
using namespace std;
void printList(const list<int>& l) {
for (auto x : l) {
cout << x << " ";
}
cout << endl;
}
int main() {
list<int> l;
// 尾插尾删
l.push_back(1);
l.push_back(2);
l.push_back(3);
cout << "push_back后:";
printList(l); // 1 2 3
l.pop_back();
cout << "pop_back后:";
printList(l); // 1 2
// 头插头删(vector没有push_front!)
l.push_front(0);
l.push_front(-1);
cout << "push_front后:";
printList(l); // -1 0 1 2
l.pop_front();
cout << "pop_front后:";
printList(l); // 0 1 2
return 0;
}
2. insert、erase
简介作用
insert:在指定位置插入元素 erase:删除指定位置的元素
这两个是 list 的核心优势!在任意位置插入删除都是 O (1)(前提是已经找到位置)。
代码示例
cpp
#include <iostream>
#include <list>
using namespace std;
void printList(const list<int>& l) {
for (auto x : l) {
cout << x << " ";
}
cout << endl;
}
int main() {
list<int> l;
for (int i = 1; i <= 5; i++) {
l.push_back(i);
}
cout << "原始:";
printList(l); // 1 2 3 4 5
// 找到第3个元素的位置
list<int>::iterator it = l.begin();
++it; ++it; // 现在指向3
// 示例1:在it位置插入一个元素
l.insert(it, 30); // 在3前面插入30
cout << "insert后:";
printList(l); // 1 2 30 3 4 5
// 示例2:在it位置插入n个元素
it = l.begin();
l.insert(it, 3, 0); // 在开头插入3个0
cout << "insert n个后:";
printList(l); // 0 0 0 1 2 30 3 4 5
// 示例3:删除it位置的元素
it = l.begin();
++it; // 指向第二个0
l.erase(it);
cout << "erase后:";
printList(l); // 0 0 1 2 30 3 4 5
// 示例4:删除区间元素
it = l.begin();
auto end = l.begin();
++end; ++end; // 指向1
l.erase(it, end); // 删除[begin, end)区间
cout << "erase区间后:";
printList(l); // 1 2 30 3 4 5
return 0;
}
🐛 经典 Bug 和坑点
坑点:erase 会导致迭代器失效!
cpp
list<int>::iterator it = l.begin();
l.erase(it); // 删除后it就失效了!
// ++it; // ❌ 错误!it已经失效
正确写法:
cpp
it = l.erase(it); // erase会返回下一个有效迭代器
💡 注意:list 的 insert不会导致其他迭代器失效,这和 vector 不一样!
3. swap、clear
简介作用
-
swap:交换两个 list 的内容,O (1)!只交换头节点指针 -
clear:清空所有元素
代码示例
cpp
#include <iostream>
#include <list>
using namespace std;
void printList(const list<int>& l, const string& name) {
cout << name << ": ";
for (auto x : l) {
cout << x << " ";
}
cout << endl;
}
int main() {
list<int> l1 = {1, 2, 3};
list<int> l2 = {10, 20, 30, 40};
printList(l1, "l1"); // l1: 1 2 3
printList(l2, "l2"); // l2: 10 20 30 40
// 交换两个list
l1.swap(l2);
printList(l1, "交换后l1"); // 交换后l1: 10 20 30 40
printList(l2, "交换后l2"); // 交换后l2: 1 2 3
// 清空
l1.clear();
cout << "clear后l1的大小:" << l1.size() << endl; // 0
return 0;
}
4. sort、reverse、unique(list 特有接口)
简介作用
为什么 list 要有自己的 sort? 因为算法库的 sort 要求随机访问迭代器,list 不支持,所以必须自己实现。
-
sort():排序(默认升序) -
reverse():反转链表 -
unique():去重(注意:只会去掉相邻的重复元素,所以一般先排序再去重)
代码示例
cpp
#include <iostream>
#include <list>
using namespace std;
void printList(const list<int>& l) {
for (auto x : l) {
cout << x << " ";
}
cout << endl;
}
int main() {
list<int> l = {3, 1, 4, 1, 5, 9, 2, 6};
cout << "原始:";
printList(l); // 3 1 4 1 5 9 2 6
// 反转
l.reverse();
cout << "reverse后:";
printList(l); // 6 2 9 5 1 4 1 3
// 排序
l.sort();
cout << "sort后:";
printList(l); // 1 1 2 3 4 5 6 9
// 去重(必须先排序!)
l.unique();
cout << "unique后:";
printList(l); // 1 2 3 4 5 6 9
// 自定义排序:降序
l.sort(greater<int>());
cout << "降序排序:";
printList(l); // 9 6 5 4 3 2 1
return 0;
}
🐛 经典 Bug 和坑点
坑点:unique 只去相邻重复!
cpp
list<int> l = {1, 2, 1, 2, 1};
l.unique(); // 结果还是{1, 2, 1, 2, 1},因为没有相邻重复!
// 正确做法:先排序再去重
l.sort();
l.unique(); // 结果{1, 2}
(六)vector 和 list 的对比总结(面试必背)
|-----------|--------------------------|----------------------------|
| 对比项 | vector | list |
| 底层结构 | 动态连续数组 | 带头双向循环链表 |
| 随机访问 | ✅ 支持 \[\],O (1) | ❌ 不支持,O (n) |
| 插入删除 | 头尾 O (1),中间 O (n) | 任意位置 O (1) |
| 迭代器类型 | 随机访问迭代器(原生指针) | 双向迭代器(封装类) |
| 空间利用率 | 高,会预留空间 | 低,每个节点多两个指针 |
| 缓存利用率 | 高,空间连续 | 低,节点分散 |
| 迭代器失效 | insert/erase 可能导致所有迭代器失效 | erase 只导致被删节点失效,insert 不失效 |
适用场景:
-
用 vector:需要随机访问、尾插尾删多、元素小、追求效率
-
用 list:频繁中间插入删除、不需要随机访问、元素大拷贝成本高
💡 面试金句:vector 和 list 没有绝对的好坏,要看场景。90% 的情况用 vector 就够了,因为 CPU 缓存命中高,实际运行更快。只有真的需要频繁中间插入时才考虑 list。
第二部分:list 的模拟实现
现在我们来自己实现一个 list!这是面试手写代码的高频题,一定要掌握。
我们实现的是带头双向循环链表,这是 STL 的标准实现。
(一)节点结构定义
简介作用
链表的每个节点包含:数据、前驱指针、后继指针。
代码实现
cpp
// 节点结构体
template<class T>
struct ListNode {
T _data; // 数据
ListNode<T>* _prev; // 前驱指针
ListNode<T>* _next; // 后继指针
// 节点的构造函数
ListNode(const T& x = T())
: _data(x)
, _prev(nullptr)
, _next(nullptr)
{}
};
🐛 经典 Bug 和坑点
坑点:给默认值! 节点构造函数一定要给默认值,这样才能创建空节点(头节点不需要数据)。
(二)迭代器类的实现(面试高频考点!)
简介作用
重点中的重点! list 迭代器不是原生指针,是封装了节点指针的类。
我们需要重载这些运算符:
-
++、--:让迭代器移动到下一个 / 上一个节点 -
*:解引用,获取节点的数据 -
->:访问节点数据的成员 -
==、!=:比较两个迭代器是否相等
还要区分普通迭代器和 const 迭代器!
代码实现
cpp
// 迭代器类
// 模板参数技巧:用Ref和Ptr来区分普通和const迭代器
template<class T, class Ref, class Ptr>
struct ListIterator {
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node; // 封装的节点指针
// 构造函数
ListIterator(Node* node)
: _node(node)
{}
// *it 解引用
Ref operator*() {
return _node->_data;
}
// it-> 访问成员
Ptr operator->() {
return &_node->_data;
}
// ++it 前置++
Self& operator++() {
_node = _node->_next;
return *this;
}
// it++ 后置++
Self operator++(int) {
Self tmp(*this);
_node = _node->_next;
return tmp;
}
// --it 前置--
Self& operator--() {
_node = _node->_prev;
return *this;
}
// it-- 后置--
Self operator--(int) {
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
// ==
bool operator==(const Self& it) const {
return _node == it._node;
}
// !=
bool operator!=(const Self& it) const {
return _node != it._node;
}
};
🐛 经典 Bug 和坑点
坑点 1:模板参数的巧妙设计
cpp
// 普通迭代器:Ref=T&, Ptr=T*
typedef ListIterator<T, T&, T*> iterator;
// const迭代器:Ref=const T&, Ptr=const T*
typedef ListIterator<T, const T&, const T*> const_iterator;
用模板参数来复用代码,不用写两个迭代器类!
坑点 2:-> 运算符的重载
cpp
// 假设T是一个结构体
struct A { int a; int b; };
list<A> la;
la.push_back({1, 2});
auto it = la.begin();
cout << it->a << endl; // 等价于 (*it).a
// it-> 返回&A::_data,编译器会自动再应用一次->
💡 面试考点:list 迭代器为什么要封装成类?
节点不连续,原生指针 ++ 不行,必须重载 ++ 指向下一个节点
封装后使用方式和指针一样,符合 STL 迭代器规范
可以区分普通和 const 迭代器
(三)list 的核心结构
简介作用
list 只需要一个成员变量:头节点指针。
带头循环链表的好处:尾插尾删不用找尾,头节点的_prev 就是尾!
代码实现
cpp
template<class T>
class list {
public:
typedef ListNode<T> Node;
// 迭代器类型定义
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
private:
Node* _head; // 头节点指针
};
(四)默认成员函数
1. 无参构造
简介作用
创建空链表,初始化头节点,让头节点自己指向自己形成循环。
代码实现
cpp
// 初始化头节点
void empty_init() {
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
// 无参构造
list() {
empty_init();
}
🐛 经典 Bug 和坑点
坑点:头节点一定要自己成环!
cpp
// 错误:没有成环
_head = new Node; // _next和_prev都是nullptr
// 正确:自己指向自己
_head->_next = _head;
_head->_prev = _head;
不然后面的尾插会访问空指针!
2. 拷贝构造
简介作用
深拷贝,创建一个全新的链表。
代码实现
cpp
// 拷贝构造
list(const list<T>& lt) {
empty_init(); // 先创建自己的头节点
// 把lt的元素一个个尾插过来
for (auto& x : lt) {
push_back(x);
}
}
坑点:一定要先初始化自己的头节点! 拷贝构造是创建新对象,必须先调用 empty_init () 创建自己的头节点,不然_head 是野指针。
3. 析构函数
简介作用
释放所有节点,包括头节点。
代码实现
cpp
// 析构函数
~list() {
clear(); // 先清掉所有数据节点
delete _head; // 再删掉头节点
_head = nullptr;
}
4. 赋值运算符重载
简介作用
现代写法:利用拷贝构造 + swap。
代码实现
cpp
// 赋值运算符重载(现代写法)
list<T>& operator=(list<T> lt) { // 传值调用拷贝构造
swap(lt); // 和临时对象交换
return *this;
}
// 交换两个list
void swap(list<T>& lt) {
std::swap(_head, lt._head);
}
(五)常用接口实现
1. begin、end、size、empty
简介作用
-
begin ():第一个数据节点(头节点的下一个)
-
end ():头节点本身(哨兵位)
-
带头循环链表的 end 就是头节点!
代码实现
cpp
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);
}
size_t size() const {
size_t count = 0;
const_iterator it = begin();
while (it != end()) {
++count;
++it;
}
return count;
}
bool empty() const {
return _head->_next == _head;
}
2. push_back、pop_back、push_front、pop_front
简介作用
有了带头循环链表,尾插太简单了!头节点的_prev 就是尾。
代码实现
cpp
// 在pos位置之前插入
iterator insert(iterator pos, const T& x) {
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
// 尾插
void push_back(const T& x) {
insert(end(), x); // 在end(头节点)前插入就是尾插
}
// 头插
void push_front(const T& x) {
insert(begin(), x); // 在begin前插入就是头插
}
// 删除pos位置的节点
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);
}
// 尾删
void pop_back() {
erase(--end());
}
// 头删
void pop_front() {
erase(begin());
}
3. insert、erase
简介作用
insert 和 erase 是核心,上面已经实现了。
🐛 经典 Bug 和坑点
坑点:erase 的迭代器失效
cpp
// 错误写法
for (auto it = l.begin(); it != l.end(); ++it) {
if (*it % 2 == 0) {
l.erase(it); // erase后it失效了!
}
}
// 正确写法
auto it = l.begin();
while (it != l.end()) {
if (*it % 2 == 0) {
it = l.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
4. clear、swap
代码实现
cpp
// 清空所有数据节点
void clear() {
iterator it = begin();
while (it != end()) {
it = erase(it);
}
}
// 交换
void swap(list<T>& lt) {
std::swap(_head, lt._head);
}
(六)list 迭代器失效问题(面试考点)
和 vector 对比:
|------------|--------------------|--------------------------|
| 操作 | vector 迭代器失效 | list 迭代器失效 |
| insert | ✅ 所有迭代器都可能失效(空间扩容) | ❌ 只有被插入位置的迭代器?不!都不失效 |
| erase | ✅ 当前及之后的迭代器都失效 | ❌ 只有被删除的那个迭代器失效 |
原因:
-
vector 是连续空间,insert 可能扩容,地址都变了,所有迭代器都失效
-
list 每个节点独立,insert 只是改指针,其他节点地址都没变,所以迭代器不失效
-
erase 时,vector 后面的元素会往前移,所以后面的迭代器都失效
-
list erase 只删那个节点,其他节点不动,所以只有被删的那个失效
完整测试代码
cpp
#include <iostream>
using namespace std;
// 把上面所有实现放这里...
int main() {
list<int> l;
// 测试push_back
l.push_back(1);
l.push_back(2);
l.push_back(3);
// 测试迭代器遍历
cout << "遍历:";
for (auto x : l) {
cout << x << " ";
}
cout << endl;
// 测试insert
auto it = l.begin();
++it;
l.insert(it, 20);
// 测试erase
l.erase(l.begin());
cout << "操作后:";
for (auto x : l) {
cout << x << " ";
}
cout << endl;
return 0;
}
总结
今天我们学习了 list 的使用和模拟实现,重点掌握:
-
list 和 vector 的区别:数组 vs 链表,随机访问 vs 插入删除
-
list 迭代器的封装:不是原生指针,重载运算符
-
带头双向循环链表:头节点的妙用
-
迭代器失效:list 只有 erase 会导致当前迭代器失效
下一篇我们将学习 STL 中的另一个容器 ------stack 和 queue,敬请期待!
创作不易,如果觉得有帮助,欢迎点赞、收藏、关注!有问题可以在评论区留言~