STL list容器全面解析:使用、模拟实现与vector对比
在C++的STL序列式容器中,list是一款基于双向循环链表实现的容器,与vector形成了鲜明的特性互补,在大量插入、删除操作的场景中有着不可替代的作用。本文将从list的基础使用、迭代器特性、模拟实现,以及与vector的核心对比几个方面,全面解析list容器的核心知识点,帮助大家掌握其使用方法与底层逻辑。
一、list容器的基础介绍与常用接口
list的底层结构为带头结点的双向循环链表,这一结构决定了它在任意位置的插入、删除操作上的效率优势,同时也让它不支持随机访问。list提供了丰富的接口,掌握核心接口的使用是灵活运用list的基础,以下为核心常用接口的详细说明与使用要点。
1.1 list的构造函数
list提供了四种核心构造方式,满足不同的初始化需求,接口说明如下表:
| 构造函数 | 接口说明 |
|---|---|
| list (size_type n, const value_type& val = value_type()) | 构造包含n个值为val的元素的list |
| list() | 构造空的list |
| list (const list& x) | 拷贝构造函数,复用已有list的元素构造新list |
| list (InputIterator first, InputIterator last) | 用[first, last)区间中的元素构造list |
1.2 list迭代器的使用
可以将list迭代器暂时理解为指向链表节点的"指针",分为正向迭代器 和反向迭代器,核心接口及特性如下:
- begin() + end() :begin返回第一个元素的迭代器,end返回最后一个元素下一个位置的迭代器,为正向迭代器;
- rbegin() + rend():rbegin返回指向end位置的反向迭代器,rend返回指向begin位置的反向迭代器,为反向迭代器。
迭代器核心注意点:
- 正向迭代器执行
++操作,迭代器向后移动; - 反向迭代器执行
++操作,迭代器向前移动(反向迭代器的++等价于正向迭代器的--)。
1.3 list的容量判断
提供两个简单的容量相关接口,用于获取容器的基础容量信息:
- empty():检测list是否为空,为空返回true,否则返回false;
- size():返回list中有效节点的个数。
1.4 list的元素访问
由于list不支持随机访问(无法通过[]或at()访问),仅提供两个接口访问首尾元素,且返回的是元素的引用,支持直接修改:
- front():返回list第一个节点中值的引用;
- back():返回list最后一个节点中值的引用。
1.5 list的修改操作
list的修改接口是其核心,充分体现了链表结构插入、删除的效率优势,核心接口如下表:
| 函数声明 | 接口说明 |
|---|---|
| push_front(val) | 在list首元素前插入值为val的元素 |
| pop_front() | 删除list中第一个元素 |
| push_back(val) | 在list尾部插入值为val的元素 |
| pop_back() | 删除list中最后一个元素 |
| insert(pos, val) | 在list的pos迭代器位置插入值为val的元素 |
| erase(pos) | 删除list的pos迭代器位置的元素 |
| swap(lst) | 交换两个list中的所有元素 |
| clear() | 清空list中的所有有效元素 |
1.6 list的迭代器失效问题
迭代器失效的本质是迭代器指向的节点变为无效(被删除),list的迭代器失效特性由其底层链表结构决定,与vector有显著区别:
- 插入操作:不会导致任何迭代器失效,因为链表插入仅改变节点指针指向,不会挪动原有节点;
- 删除操作 :仅导致指向被删除节点的迭代器失效,其他迭代器不受任何影响。
典型错误与改正 :
删除元素时,若直接使用迭代器遍历删除,会因迭代器失效导致程序崩溃,正确的写法有两种:
c++
// 错误写法:erase后it指向的节点被删除,it失效,++it无意义
void TestListIterator1() {
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array+sizeof(array)/sizeof(array[0]));
auto it = l.begin();
while (it != l.end()) {
l.erase(it);
++it;
}
}
// 正确写法1:利用it++的特性,先传参再自增
void TestListIterator() {
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array+sizeof(array)/sizeof(array[0]));
auto it = l.begin();
while (it != l.end()) {
l.erase(it++);
}
}
// 正确写法2:接收erase的返回值(erase返回被删除节点下一个位置的迭代器)
void TestListIterator2() {
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array+sizeof(array)/sizeof(array[0]));
auto it = l.begin();
while (it != l.end()) {
it = l.erase(it);
}
}
二、list容器的模拟实现
要实现list的模拟实现,核心是还原其带头结点的双向循环链表 底层结构,同时实现核心接口与反向迭代器 。反向迭代器的实现是模拟list的重点与难点,其核心思路是基于正向迭代器进行包装。
2.1 核心底层结构
首先实现list的节点结构,包含数据域和两个指针域(前驱、后继),再实现list的容器主体,包含头节点指针,完成基础框架搭建。
2.2 反向迭代器的模拟实现
反向迭代器的核心特性是:++等价于正向迭代器的--,--等价于正向迭代器的++。因此可以将正向迭代器作为反向迭代器的成员变量,对其接口进行封装,核心实现代码如下:
c++
template<class Iterator>
class ReverseListIterator
{
public:
// 明确Ref和Ptr是Iterator的内嵌类型
typedef typename Iterator::Ref Ref;
typedef typename Iterator::Ptr Ptr;
typedef ReverseListIterator<Iterator> Self;
// 构造函数:接收正向迭代器
ReverseListIterator(Iterator it): _it(it){}
// 重载*:反向迭代器解引用指向正向迭代器前一个节点
Ref operator*(){
Iterator temp(_it);
--temp;
return *temp;
}
// 重载->:返回解引用结果的地址
Ptr 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& l)const {
return _it != l._it;
}
bool operator==(const Self& l)const {
return _it == l._it;
}
private:
Iterator _it; // 封装正向迭代器
};
关键要点 :使用typename明确Iterator::Ref和Iterator::Ptr是内嵌类型,避免编译器将其解析为静态成员变量。
三、list与vector的核心对比
vector和list是STL中最常用的两个序列式容器,二者底层结构不同,导致特性、效率、应用场景天差地别。以下为二者的核心对比,清晰呈现各自的优劣:
| 对比维度 | vector | list |
|---|---|---|
| 底层结构 | 动态顺序表,一段连续的内存空间 | 带头结点的双向循环链表 |
| 随机访问 | 支持,访问任意元素效率O(1) | 不支持,访问任意元素效率O(N) |
| 插入/删除 | 任意位置操作效率低,需搬移元素,时间复杂度O(N);插入可能触发增容(开辟新空间+拷贝元素+释放旧空间),效率更低 | 任意位置操作效率高,无需搬移元素,时间复杂度O(1) |
| 空间利用率 | 连续空间,不易产生内存碎片,空间利用率高,缓存利用率高 | 节点动态开辟,小节点易产生内存碎片,空间利用率低,缓存利用率低 |
| 迭代器本质 | 原生态指针(指向连续空间的地址) | 对节点指针的封装,非原生态指针 |
| 迭代器失效 | 插入:可能因增容导致所有迭代器失效;删除:仅当前迭代器失效,需重新赋值 | 插入:无迭代器失效;删除:仅指向被删节点的迭代器失效 |
| 核心应用场景 | 需高效存储、频繁随机访问,对插入/删除效率要求低的场景 | 需大量插入、删除操作,对随机访问无要求的场景 |
四、总结
- list的底层是带头结点的双向循环链表 ,这一结构决定了它的核心优势是任意位置插入/删除效率高(O(1)) ,劣势是不支持随机访问;
- list的迭代器失效仅发生在删除操作时,且仅失效指向被删节点的迭代器,插入操作无迭代器失效问题,使用时需注意迭代器的正确更新;
- 反向迭代器的实现可基于正向迭代器封装,通过重载运算符实现反向的移动与访问,是list模拟实现的核心考点;
- list与vector的选择需根据业务场景判断:频繁随机访问选vector,大量插入/删除选list,二者形成特性互补,覆盖绝大多数序列式容器的使用场景。
掌握list的核心特性与接口使用,结合其与vector的对比,能让我们在实际开发中精准选择合适的容器,提升程序的运行效率。