
C++专栏:C++_Yupureki的博客-CSDN博客
目录
[1. list简介](#1. list简介)
[1.1 什么是list?](#1.1 什么是list?)
[1.2 list的底层结构](#1.2 list的底层结构)
[2. list的基本使用](#2. list的基本使用)
[2.1 构造list](#2.1 构造list)
[2.2 迭代器使用](#2.2 迭代器使用)
[2.3 容量操作](#2.3 容量操作)
[2.4 元素访问](#2.4 元素访问)
[2.5 修改操作](#2.5 修改操作)
[2.6 list特有操作](#2.6 list特有操作)
[3. 迭代器失效问题](#3. 迭代器失效问题)
[3.1 安全的操作(不会导致迭代器失效)](#3.1 安全的操作(不会导致迭代器失效))
[3.2 危险的操作(会导致当前迭代器失效)](#3.2 危险的操作(会导致当前迭代器失效))
[3.3 批量删除的正确方式](#3.3 批量删除的正确方式)
[4. list的模拟实现](#4. list的模拟实现)
[4.1 节点结构](#4.1 节点结构)
[4.2 list基础框架](#4.2 list基础框架)
[4.3 反向迭代器实现](#4.3 反向迭代器实现)
[5. list与vector的对比](#5. list与vector的对比)
[6. 性能优化建议](#6. 性能优化建议)
上一篇:从零开始的C++学习生活 7:vector的入门使用-CSDN博客
前言
在C++标准模板库(STL)中,list
是一个基于双向链表实现的序列容器,与vector
的连续内存布局形成鲜明对比。虽然vector
因其随机访问特性而广受欢迎,但在某些场景下,list
的插入删除效率和无序存储特性使其成为不可替代的选择。
list中每个元素独立存储,通过指针相互连接。这种结构使得在任意位置的插入和删除操作都能在常数时间内完成,但代价是失去了随机访问的能力。
本文将深入探讨list
的各个方面,从基本使用到底层实现,帮助你理解何时以及如何正确使用这个重要的容器。

1. list简介
1.1 什么是list?
list
是C++标准库中的一个序列容器,它基于带头节点的双向循环链表实现。主要特点包括:
-
双向遍历:支持从前向后和从后向前的遍历
-
高效插入删除:在任意位置插入删除元素的时间复杂度为O(1)
-
非连续存储:元素在内存中分散存储,无扩容开销
-
迭代器稳定性:插入操作不会使迭代器失效
1.2 list的底层结构
简易的list由3个类构成:list本体(集成功能函数,头节点和节点个数),listnode(单个list的节点),iterator(list专属的迭代器)
cpp
template<class T>
class List {//list本体的结构
typedef ListNode<T> Node;//list节点
typedef List_Iterator<T> iterator;//迭代器
public:
//函数实现
......
private:
Node* head;//链表的哨兵位
size_t size;//链表的有效结点个数(不包含哨兵位)
};
cpp
// list的节点结构示意
struct ListNode {
T data; // 存储的数据
ListNode* prev; // 指向前驱节点
ListNode* next; // 指向后继节点
};
cpp
//list的迭代器
template<class T>
struct List_Iterator {
typedef ListNode<T> Node;
typedef List_Iterator iterator;
Node* node;
......
};
list的整体结构是一个带头节点的双向循环链表:
-
头节点的
prev
指向尾节点 -
尾节点的
next
指向头节点 -
头节点不存储实际数据
为什么要集成list的迭代器?
由于物理空间的特性,list不像string和vector是线性的,普通的++,--,*和其他操作符无法直接用在节点的指针上,而为了保持统一,例如我们当然也得实现list的迭代器++指向下一个节点,*表示该节点的值
2. list的基本使用
2.1 构造list
list和srting,vector的构造极为相似
cpp
#include <list>
using namespace std;
// 1. 默认构造 - 空list
list<int> l1;
// 2. 构造包含n个val的list
list<int> l2(5, 10); // {10, 10, 10, 10, 10}
// 3. 拷贝构造
list<int> l3(l2); // 与l2相同
// 4. 使用迭代器范围构造
int arr[] = {1, 2, 3, 4, 5};
list<int> l4(arr, arr + 5); // {1, 2, 3, 4, 5}
// 5. 初始化列表构造 (C++11)
list<int> l5 = {1, 2, 3, 4, 5};
2.2 迭代器使用
集成后的迭代器可像string和vector那样使用
cpp
list<int> lst = {1, 2, 3, 4, 5};
// 正向迭代器
cout << "正向遍历: ";
for (auto it = lst.begin(); it != lst.end(); ++it) {
cout << *it << " "; // 1 2 3 4 5
}
// 反向迭代器
cout << "\n反向遍历: ";
for (auto rit = lst.rbegin(); rit != lst.rend(); ++rit) {
cout << *rit << " "; // 5 4 3 2 1
}
// 范围for循环
cout << "\n范围for: ";
for (auto& elem : lst) {
cout << elem << " ";
}
注意 :list的迭代器是双向迭代器 ,不支持随机访问,不能进行it + n
操作。
2.3 容量操作
list没有capacity容量的概念,只有size有效节点个数的概念
cpp
list<int> lst = {1, 2, 3};
cout << lst.size(); // 元素个数: 3
cout << lst.empty(); // 是否为空: false
cout << lst.max_size(); // 理论最大容量
// list没有capacity概念,因为不需要预分配空间
2.4 元素访问
list和string,vector不同的是,list不是线性的,不能使用[]下标来访问数据(实际上也可以实现,但不觉的别扭?链表用[]来访问?)
cpp
list<int> lst = {1, 2, 3, 4, 5};
// 访问首尾元素
cout << lst.front(); // 1
cout << lst.back(); // 5
// 注意:list不支持下标访问!
// cout << lst[0]; // 错误!编译不通过
2.5 修改操作
list的增删查改与string,vector也基本一致
cpp
list<int> lst = {1, 2, 3};
// 头部操作
lst.push_front(0); // {0, 1, 2, 3}
lst.pop_front(); // {1, 2, 3}
// 尾部操作
lst.push_back(4); // {1, 2, 3, 4}
lst.pop_back(); // {1, 2, 3}
// 插入操作
auto it = lst.begin();
++it; // 指向第二个元素
lst.insert(it, 10); // {1, 10, 2, 3}
// 删除操作
it = lst.begin();
++it; // 指向10
lst.erase(it); // {1, 2, 3}
// 清空
lst.clear(); // {}
// 交换
list<int> lst2 = {4, 5, 6};
lst.swap(lst2); // 交换两个list的内容
2.6 list特有操作
list不受连续空间的限制,相对自由且高效
cpp
list<int> lst1 = {1, 3, 5};
list<int> lst2 = {2, 4, 6};
// 合并两个有序链表(lst2会被清空)
lst1.merge(lst2); // lst1: {1, 2, 3, 4, 5, 6}, lst2: {}
// 排序
list<int> lst3 = {3, 1, 4, 2};
lst3.sort(); // {1, 2, 3, 4}
// 去重(需要先排序)
list<int> lst4 = {1, 2, 2, 3, 3, 3};
lst4.unique(); // {1, 2, 3}
// 反转
list<int> lst5 = {1, 2, 3};
lst5.reverse(); // {3, 2, 1}
// 拼接:将另一个list的部分元素移动到当前list
list<int> lst6 = {1, 2, 3};
list<int> lst7 = {4, 5, 6};
auto pos = lst6.begin();
++pos; // 指向2
lst6.splice(pos, lst7); // lst6: {1, 4, 5, 6, 2, 3}, lst7: {}
3. 迭代器失效问题
与vector
不同,list
的迭代器失效规则更加简单:
3.1 安全的操作(不会导致迭代器失效)
-
插入操作 :
push_back()
,push_front()
,insert()
-
其他操作 :
merge()
,sort()
,reverse()
3.2 危险的操作(会导致当前迭代器失效)
- 删除操作 :
pop_back()
,pop_front()
,erase()
cpp
// 错误示例
list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
++it; // 指向2
lst.erase(it); // 删除2
// cout << *it; // 错误!it已失效
// 正确做法
it = lst.begin();
++it;
it = lst.erase(it); // erase返回下一个有效迭代器
cout << *it; // 现在it指向3
节点删除后直接使it成为野指针,因此会导致迭代器失效
3.3 批量删除的正确方式
cpp
list<int> lst = {1, 2, 3, 4, 5, 6};
// 删除所有偶数 - 正确方式
auto it = lst.begin();
while (it != lst.end()) {
if (*it % 2 == 0) {
it = lst.erase(it); // 重要:接收返回值
} else {
++it;
}
}
// 或者使用remove_if(更简洁)
lst.remove_if([](int x) { return x % 2 == 0; });
4. list的模拟实现
4.1 节点结构
cpp
template<typename T>
struct ListNode {
T _data;
ListNode<T>* _prev;
ListNode<T>* _next;
ListNode(const T& val = T())
: _data(val)
, _prev(nullptr)
, _next(nullptr)
{}
};
4.2 list基础框架
cpp
template<typename T>
class list {
private:
ListNode<T>* _head; // 头节点(不存储实际数据)
public:
// 迭代器类(简化版)
class iterator {
private:
ListNode<T>* _node;
public:
iterator(ListNode<T>* node = nullptr) : _node(node) {}
T& operator*() { return _node->_data; }
T* operator->() { return &(_node->_data); }
iterator& operator++() {
_node = _node->_next;
return *this;
}
iterator operator++(int) {
iterator temp = *this;
_node = _node->_next;
return temp;
}
iterator& operator--() {
_node = _node->_prev;
return *this;
}
iterator operator--(int) {
iterator temp = *this;
_node = _node->_prev;
return temp;
}
bool operator!=(const iterator& it) const {
return _node != it._node;
}
bool operator==(const iterator& it) const {
return _node == it._node;
}
};
public:
// 构造函数
list() {
_head = new ListNode<T>;
_head->_prev = _head;
_head->_next = _head; // 循环链表
}
// 析构函数
~list() {
clear();
delete _head;
_head = nullptr;
}
// 迭代器相关
iterator begin() { return iterator(_head->_next); }
iterator end() { return iterator(_head); }
// 容量相关
bool empty() const { return _head->_next == _head; }
size_t size() const {
size_t count = 0;
ListNode<T>* cur = _head->_next;
while (cur != _head) {
++count;
cur = cur->_next;
}
return count;
}
// 元素访问
T& front() { return _head->_next->_data; }
T& back() { return _head->_prev->_data; }
// 修改操作
void push_back(const T& val) {
insert(end(), val);
}
void push_front(const T& val) {
insert(begin(), val);
}
void pop_back() {
erase(iterator(_head->_prev));
}
void pop_front() {
erase(begin());
}
// 插入操作
iterator insert(iterator pos, const T& val) {
ListNode<T>* newNode = new ListNode<T>(val);
ListNode<T>* cur = pos._node;
ListNode<T>* prev = cur->_prev;
// 调整指针
newNode->_prev = prev;
newNode->_next = cur;
prev->_next = newNode;
cur->_prev = newNode;
return iterator(newNode);
}
// 删除操作
iterator erase(iterator pos) {
ListNode<T>* cur = pos._node;
ListNode<T>* prev = cur->_prev;
ListNode<T>* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
void clear() {
ListNode<T>* cur = _head->_next;
while (cur != _head) {
ListNode<T>* next = cur->_next;
delete cur;
cur = next;
}
_head->_next = _head;
_head->_prev = _head;
}
};
4.3 反向迭代器实现
cpp
template<class Iterator>
class ReverseListIterator {
private:
Iterator _it;
public:
typedef ReverseListIterator<Iterator> Self;
ReverseListIterator(Iterator it) : _it(it) {}
// 解引用:需要向前移动一位,因为反向迭代器指向实际的前一个位置
typename Iterator::reference operator*() {
Iterator temp = _it;
--temp;
return *temp;
}
typename Iterator::pointer operator->() {
return &(operator*());
}
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;
}
};
5. list与vector的对比
特性 | vector | list |
---|---|---|
底层结构 | 动态数组,连续内存 | 双向链表,非连续内存 |
随机访问 | O(1),支持下标访问 | O(n),不支持下标访问 |
插入删除 | 尾部O(1),中间O(n) | 任意位置O(1) |
内存使用 | 内存连续,利用率高 | 每个元素额外存储指针 |
缓存友好 | 是,空间局部性好 | 否,空间局部性差 |
迭代器失效 | 插入删除可能导致全部失效 | 只有被删除元素迭代器失效 |
适用场景 | 随机访问频繁,尾部操作多 | 任意位置插入删除频繁 |
选择指南:
使用vector的情况:
-
需要频繁随机访问元素
-
主要在尾部进行插入删除操作
-
内存效率要求高
-
元素数量相对稳定
使用list的情况:
-
需要在任意位置频繁插入删除
-
不需要随机访问,主要是顺序访问
-
对迭代器稳定性要求高
-
元素大小较大,移动成本高
6. 性能优化建议
-
批量操作:使用范围插入而不是多次单元素插入
-
算法选择:利用list特有的sort、merge等算法
-
迭代器缓存:对于频繁访问的位置可以缓存迭代器
-
避免不必要的拷贝:使用emplace操作