对于C++中list的详细介绍

大家除夕快乐!!!祝大家新的一年都万事顺心,吉祥如意~~~~~~~~~

开篇介绍:

hello 大家,我们见面了,那么在前面几篇博客中,我们成功的模拟实现了C++中的vector,并且深度的了解了它,那么接下来,我们就要去了解一下,另外一个数据结构------list

在 C++ 标准库的序列容器家族中,std::list 是一个极具特色的存在 ------ 它以双向链表为底层实现,专为高效插入、删除操作而生,完美弥补了 std::vector 在中间位置修改元素时的性能短板。本文将从底层原理、核心特性、常用接口、实战场景到性能对比,全方位拆解 std::list,帮你彻底掌握这个 "灵活高效" 的容器。

一、开篇:为什么需要 std::list?

在学习 std::list 之前,我们先思考一个问题:有了 std::vector 这种 "万能容器",为什么还需要 std::list

答案藏在 内存布局与操作效率 的权衡中:

  • std::vector 是连续内存数组,优势是随机访问([] 操作 O (1))、缓存友好,但中间位置插入 / 删除需移动大量元素(O (n) 时间),且扩容时可能复制整个数组;
  • std::list 是双向链表,元素非连续存储,优势是任意位置插入 / 删除仅需调整指针(O (1) 时间),无需扩容,但不支持随机访问,缓存命中率较低。

简单说:频繁访问用 vector,频繁修改用 list。这也是 STL 容器设计的核心思想 ------ 没有万能容器,只有最适合场景的容器。

二、底层原理:双向链表的结构解密

要真正理解 std::list 的特性,必须先搞懂其底层结构。std::list 的本质是 双向链表,每个元素被封装在独立的 "节点" 中,节点间通过指针相互连接。

2.1 节点结构设计

一个典型的 std::list 节点包含三部分(不同编译器实现略有差异,但核心一致):

复制代码
template <typename T>
struct ListNode {
    T data;          // 数据域:存储元素值
    ListNode* prev;  // 前驱指针:指向前一个节点
    ListNode* next;  // 后继指针:指向后一个节点

    // 节点构造函数
    ListNode(const T& val) : data(val), prev(nullptr), next(nullptr) {}
};

2.2 链表整体结构

std::list 通常会使用 哨兵节点(Sentinel Node)(也叫 "伪头节点""伪尾节点")来简化边界处理。哨兵节点不存储有效数据,仅用于标记链表的首尾,使得空链表和非空链表的操作逻辑统一。

结构示意图(以存储 int 为例):

复制代码
[哨兵头节点] <-> [节点1: data=1] <-> [节点2: data=2] <-> [节点3: data=3] <-> [哨兵尾节点]
       ↑                                                  ↑
    begin()                                              end()
  • begin():返回指向第一个有效节点(节点 1)的迭代器;
  • end():返回指向哨兵尾节点的迭代器(尾后迭代器,不指向有效元素);
  • 空链表时,哨兵头节点的 next 指向哨兵尾节点,prev 也指向哨兵尾节点。

2.3 核心优势的底层支撑

正是这种双向链表结构,赋予了 std::list 两大核心优势:

  1. 插入 / 删除高效 :无需移动元素,仅需修改相邻节点的 prevnext 指针(比如在节点 1 和节点 2 之间插入新节点,只需让节点 1 的 next 指向新节点,新节点的 prev 指向节点 1,新节点的 next 指向节点 2,节点 2 的 prev 指向新节点);
  2. 无需扩容 :插入元素时动态分配单个节点,删除时释放单个节点,不会像 std::vector 那样因扩容复制大量元素。

三、核心特性:std::list 的关键特点

3.1 非连续内存存储

  • 元素在内存中分散存储,节点地址无需连续;
  • 优点:插入 / 删除不影响其他节点的内存地址,迭代器(除被删除节点的迭代器)始终有效;
  • 缺点:不支持随机访问(无法通过下标 []at() 直接访问第 n 个元素,需从头部 / 尾部遍历,时间复杂度 O (n))。

3.2 双向迭代器

std::list 的迭代器是 双向迭代器(BidirectionalIterator),支持:

  • ++it:向后移动到下一个元素;
  • --it:向前移动到上一个元素;
  • 解引用 *it 访问元素;
  • 不支持随机访问操作(如 it + 5it -= 2),因此依赖随机访问迭代器的算法(如 std::sort)无法直接使用,需用 list 自带的 sort 成员函数。

3.3 迭代器有效性

std::list 的迭代器有效性规则是其一大亮点,对比 std::vector 更友好:

  • 插入元素后:所有迭代器(包括指向插入位置的迭代器)均有效(节点未移动,仅指针调整);
  • 删除元素后:仅指向被删除节点的迭代器失效,其他迭代器(包括前后节点的迭代器)仍有效;
  • 对比 std::vectorvector 插入可能导致扩容(所有迭代器失效),删除中间元素会导致后续迭代器失效。

3.4 内存开销

每个节点需额外存储两个指针(prevnext),因此内存开销比 std::vector 大:

  • 对于 64 位系统,每个指针占 8 字节,一个存储 int(4 字节)的节点总占用 4 + 8 + 8 = 20 字节,指针开销占 80%;
  • 对于大数据类型(如自定义类 Person),指针开销占比会降低,内存效率会提升。

四、常用接口:从基础到进阶(附示例)

std::list 的接口设计遵循 STL 容器的统一风格,同时提供了链表特有的操作(如 splicemerge)。以下是分类整理的核心接口,结合示例代码帮助理解。

4.1 构造与析构

接口 功能 示例
list() 默认构造:创建空链表 std::list<int> l;
list(size_t n, const T& val) 构造包含 n 个 val 的链表 std::list<int> l(3, 10); // {10, 10, 10}
list(InputIt first, InputIt last) 用迭代器范围构造 std::vector<int> v = {1,2,3}; std::list<int> l(v.begin(), v.end());
list(const list& other) 拷贝构造 std::list<int> l1 = {1,2}; std::list<int> l2(l1);
list(list&& other) 移动构造(C++11) std::list<int> l1 = {1,2}; std::list<int> l2(std::move(l1)); // l1为空
~list() 析构:销毁所有元素并释放节点内存 -

4.2 迭代器与遍历

接口 功能
begin() / end() 普通迭代器(首元素 / 尾后)
rbegin() / rend() 反向迭代器(尾元素 / 首前)
cbegin() / cend() const 迭代器(只读)

遍历示例

复制代码
std::list<int> l = {1,2,3,4,5};

// 1. 范围for循环(推荐)
for (int val : l) {
    std::cout << val << " "; // 输出:1 2 3 4 5
}

// 2. 普通迭代器
for (auto it = l.begin(); it != l.end(); ++it) {
    std::cout << *it << " ";
}

// 3. 反向迭代器
for (auto it = l.rbegin(); it != l.rend(); ++it) {
    std::cout << *it << " "; // 输出:5 4 3 2 1
}

4.3 容量与状态查询

接口 功能 时间复杂度
empty() 判断链表是否为空 O(1)
size() 返回元素个数(C++11 后 O (1)) O(1)
max_size() 返回理论最大容量(系统内存限制) O(1)

4.4 元素访问

std::list 不支持随机访问,仅提供首尾元素的直接访问:

接口 功能 示例
front() 返回首元素的引用(非 const) l.front() = 10; // 修改首元素为10
back() 返回尾元素的引用(非 const) std::cout << l.back(); // 输出尾元素
const_front() / const_back() 返回 const 引用(只读) -

4.5 核心修改操作

4.5.1 首尾操作(O (1))
接口 功能 示例
push_front(const T& val) 头部插入元素 l.push_front(0); // 链表变为 {0,1,2,3,4,5}
pop_front() 删除头部元素(需非空) l.pop_front(); // 链表变为 {1,2,3,4,5}
push_back(const T& val) 尾部插入元素 l.push_back(6); // 链表变为 {1,2,3,4,5,6}
pop_back() 删除尾部元素(需非空) l.pop_back(); // 链表变为 {1,2,3,4,5}
4.5.2 任意位置插入 / 删除(O (1),已知迭代器)
接口 功能 示例
insert(iterator pos, const T& val) 在 pos 前插入 val,返回新元素迭代器 auto it = l.begin(); ++it; l.insert(it, 10); // 在1后插入10,链表变为 {1,10,2,3,4,5}
insert(iterator pos, size_t n, const T& val) 在 pos 前插入 n 个 val l.insert(l.end(), 2, 6); // 尾部插入2个6,链表变为 {1,10,2,3,4,5,6,6}
erase(iterator pos) 删除 pos 指向的元素,返回下一个元素迭代器 auto it = l.begin(); ++it; it = l.erase(it); // 删除10,it指向2,链表变为 {1,2,3,4,5,6,6}
erase(iterator first, iterator last) 删除 [first, last) 范围元素 auto first = l.begin(); ++first; auto last = l.end(); l.erase(first, last); // 删除2及之后元素,链表变为 {1}
clear() 删除所有元素,链表为空 l.clear();
4.5.3 链表特有操作(核心优势)

这些操作是 std::list 区别于其他容器的关键,充分利用了链表的指针特性,效率极高:

(1)splice:元素转移(O (1) 或 O (n))

将一个链表的元素 "剪切" 到另一个链表,无需复制元素,仅调整指针。

复制代码
// 重载1:转移整个链表
void splice(iterator pos, list& other);

// 重载2:转移单个元素
void splice(iterator pos, list& other, iterator it);

// 重载3:转移范围元素
void splice(iterator pos, list& other, iterator first, iterator last);

示例

复制代码
std::list<int> l1 = {1,2,3};
std::list<int> l2 = {4,5,6};

// 转移l2的所有元素到l1尾部
l1.splice(l1.end(), l2); // l1: {1,2,3,4,5,6}, l2: 空

// 转移l1中第2个元素(2)到l1头部
auto it = l1.begin(); ++it;
l1.splice(l1.begin(), l1, it); // l1: {2,1,3,4,5,6}

// 转移l1中[3,5)范围元素到尾部
auto first = l1.begin(); std::advance(first, 2); // 指向3
auto last = l1.begin(); std::advance(last, 5);  // 指向6
l1.splice(l1.end(), l1, first, last); // l1: {2,1,6,3,4,5}
(2)merge:合并两个有序链表(O (n+m))

合并两个已排序的链表,合并后仍保持有序,源链表变为空。

复制代码
std::list<int> l1 = {1,3,5};
std::list<int> l2 = {2,4,6};
l1.merge(l2); // l1: {1,2,3,4,5,6}, l2: 空
(3)reverse:反转链表(O (n))
复制代码
std::list<int> l = {1,2,3};
l.reverse(); // l: {3,2,1}
(4)unique:移除连续重复元素(O (n))

仅保留连续重复元素的第一个,需确保链表已排序(否则仅移除相邻重复元素)。

复制代码
std::list<int> l = {1,1,2,2,3};
l.unique(); // l: {1,2,3}
(5)sort:链表排序(O (n log n))

std::sort 要求随机访问迭代器,因此 std::list 自带 sort 成员函数(基于归并排序,稳定排序)。

复制代码
std::list<int> l = {3,1,4,2,5};
l.sort(); // l: {1,2,3,4,5}

五、std::list 与 std::vector 全面对比

特性 std::list std::vector
内存布局 非连续(双向链表) 连续数组
随机访问 不支持(O (n)) 支持(O (1))
插入 / 删除(中间位置) O (1)(已知迭代器) O (n)(需移动元素)
插入 / 删除(首尾) O(1) 尾部 O (1)(无扩容),头部 O (n)
扩容 无需扩容(动态分配节点) 可能扩容(复制旧元素,O (n))
迭代器类型 双向迭代器 随机访问迭代器
迭代器有效性 插入后均有效,删除后仅被删节点迭代器失效 插入可能失效,删除后后续迭代器失效
内存开销 高(每个节点 2 个指针) 低(仅存储数据)
缓存友好性 差(节点分散) 好(连续内存,缓存命中率高)
适用场景 频繁中间插入 / 删除、元素数量不确定 频繁随机访问、元素数量稳定

六、实战场景:什么时候用 std::list?

6.1 适合的场景

  1. 实现队列 / 栈 :链表的首尾操作 O (1),比 std::vector 更适合(vector 头部操作 O (n));
  2. 频繁在中间位置插入 / 删除:如日志系统(频繁插入日志)、任务调度队列(频繁添加 / 移除任务);
  3. 元素数量不确定:无需提前预留内存,动态分配节点更灵活;
  4. 自定义数据结构:如链表、双向队列等基础数据结构的实现。

6.2 不适合的场景

  1. 需要随机访问元素 :如根据下标查找元素(优先用 std::vectorstd::deque);
  2. 元素体积小且访问频繁std::vector 的缓存友好性更优,性能更胜一筹;
  3. 内存资源紧张std::list 的额外指针开销可能成为瓶颈。

七、进阶注意事项

7.1 迭代器使用禁忌

  • 不要用 it + nit -= 2 等随机访问操作(双向迭代器不支持),需用 std::advance(it, n) 移动迭代器;
  • 删除元素后,避免使用指向被删除节点的迭代器(已失效),应使用 erase 返回的下一个有效迭代器。

7.2 自定义类型的要求

  • 若存储自定义类型,需确保该类型有默认构造函数(用于节点初始化)、拷贝构造函数 / 赋值运算符(用于元素拷贝);
  • 若自定义类型有动态资源(如 char*),需手动实现深拷贝,避免浅拷贝导致的资源泄漏。

7.3 性能优化技巧

  • 尽量使用 emplace_front()/emplace_back()/emplace()(C++11)替代 push_front()/push_back()/insert(),直接在节点中构造元素,避免临时对象拷贝;
  • 批量插入元素时,优先用 insert 的迭代器范围重载(O (n)),比循环调用 push_back() 更高效;
  • 若需频繁遍历,可考虑先用 std::vector 暂存数据(缓存友好),遍历完成后再转存回 std::list

八、总结

std::list 是 C++ 标准库中双向链表的经典实现,其设计核心是 "通过指针连接节点,实现高效的插入与删除"。它不是 std::vector 的替代品,而是互补品 ------ 两者分别针对 "修改密集型" 和 "访问密集型" 场景优化。

理解 std::list 的关键在于:

  1. 底层是双向链表,非连续内存存储;
  2. 插入 / 删除高效,但不支持随机访问;
  3. 迭代器有效性规则友好,适合频繁修改的场景。

在实际开发中,选择 std::list 还是 std::vector,本质是权衡 "访问效率" 和 "修改效率"。掌握两者的差异,才能在不同场景中做出最优选择 ------ 这也是 STL 容器设计的精髓所在。

示例代码:

那么我再给大家示例代码,帮助大家进一步理解:

cpp 复制代码
#include <iostream>
#include <list>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;

void testlistconstructor()
{
	//注意:list不是指单向链表,而是指双向带头链表
	
	//那么其实list也是和vector的初始化差不多
	//都是需要显式实例化的
	//即由我们告诉编译器,我们要往链表里面传什么类型的数据
	list<int> lt1;

	//要注意,list和vector一样,我们想要输入和输出数据
	//也都需要我们自己通过循环手动实现

	list<int> lt2(5, 2);//和vector的构造函数差不多
	//创建有5个节点的链表,同时节点里面的数据都指定为1
	//要是不传入第二个参数,那么就是默认为0或者其他类型的默认构造函数所生成的
	list<double> lt3(5);

	//注意:循环list,我们就不能再用下标访问的方式了
	//因为链表是一个一个节点,而不是像数组一样的连续的存储
	//所以呢,list中也没有提供对[]的运算符重载函数
	//所以我们就要用迭代器或者for auto循环

	list<int>::iterator it2 = lt2.begin();
	cout << "lt2:" << endl;
	while (it2 != lt2.end())
	{
		cout << *it2 << " ";
		++it2;
	}
	cout << endl;
	for (auto n : lt2)
	{
		cout << n << " ";
	}
	cout << endl;

	cout << endl;

	cout << "lt3:" << endl;
	list<double>::iterator it3 = lt3.begin();
	while (it3 != lt3.end())
	{
		cout << *it3 << " ";
		++it3;
	}
	cout << endl;
	for (auto n : lt3)
	{
		cout << n << " ";
	}
	cout << endl;

	list<int> lt4;
	lt4 = lt2;
	cout << endl;
	cout << "lt4(赋值运算符重载函数):" << endl;
	for (auto n : lt4)
	{
		cout << n << " ";
	}

	cout << endl;
	list<int> lt5(lt2.begin(),lt2.end());
	cout << endl;
	cout << "lt5(迭代器构造函数):" << endl;
	for (auto n : lt5)
	{
		cout << n << " ";
	}
	//其实总结一下,构造函数的使用是和vector如出一辙的
	//但是循环遍历list就不支持下标[]访问了
}



void testlistModifiers()
{
	//接下来就是要了解一下
	//list中的尾插尾删,头插头删
	//insert、erase等等一些比较常用的函数
	
	//而同样的,list中这些函数的操作也都是传入迭代器的
	//而不是整型下标

	//但是关于list的迭代器
	//就和vector的迭代器的使用不是一模一样了
	//是有一个比较大的区别的
	//就是在vector中的迭代器中,我们是可以直接对迭代器+一个数的
	//比如
	vector<int> v1(5, 3);
	vector<int>::iterator vit = v1.begin();
	//那么我们想要指定到v1的某一个位置时,我们是可以直接对vit进行+一个数来指定位置
	v1.insert(vit + 2, 4);//可以看到,可以直接对vit迭代器+2
	cout << "v1.insert:" << endl;
	for (auto n : v1)
	{
		cout << n << " ";
	}
	cout << endl;
	cout << endl;
	//但是在list的迭代器中,就不能像上面一样直接对迭代器进行+一个数来移动迭代器的指向
	//因为list迭代器是单向迭代器,它只支持++和--
	//是不支持直接+一个数的,这是一个很重要也很大的区别,一定要注意
	//那么要怎么让list的迭代器移动多个位置呢?
	//其实就是要借助循环,去让迭代器++或者--n次,就能移动到了
	//比如下面这样子:
	list<int> lt1(5, 6);
	list<int>::iterator lit1 = lt1.begin();
	for (auto n : lt1)
	{
		cout << n << " ";
	}
	cout << endl;
	//比如想要实现像上面的对迭代器+2
	//即让迭代器移动到指向第三个节点去
	//(从1开始计数的话就是第三个节点,从0开始计数的话就是第二个节点)
	//但是由于链表不像数组,它就是从1开始计数的
	//所以我们指定为2,其实就是第二个节点,而不是像数组的第三个数据
	//(即从第一个节点开始往后走两步)
	for (int i = 0; i < 1; i++)
	{
		++lit1;
	}
	//出了循环之后,就达到了我们所指定的位置
	lt1.insert(lit1,2);
	cout << "在原本的第二个节点之前插入数据(即在第2个节点插入数据):" << endl;
	for (auto n : lt1)
	{
		cout << n << " ";
	}
	cout << endl;

	//上面说到的是非常重要的知识点。
	//在list里面,就没有reserve函数了,需要注意
	//因为本质上就是需要一个节点,就创建一个节点,不用去提前预留空间
	//这也是链表比顺序表好的一个点

	list<int> lt2;
	for (int i = 1; i <= 5; ++i)
	{
		lt2.push_back(i);//直接尾插就完事了
	}
	cout << endl;
	cout << "lt2:" << endl;

	for (auto n : lt2)
	{
		cout << n <<"->";
	}
	cout << "nullptr";
	cout << endl;

	cout << endl;

	//尾删
	lt2.pop_back();//不用传入参数,尾删哪里需要参数
	cout << "lt2 pop_back:" << endl;
	for (auto n : lt2)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;
	cout << endl;

	//头插
	lt2.push_front(0);//不用传入参数,尾删哪里需要参数
	cout << "lt2 pushfront:" << endl;
	for (auto n : lt2)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;
	cout << endl;


	//头删
	lt2.pop_front();//不用传入参数,尾删哪里需要参数
	cout << "lt2 pop_front:" << endl;
	for (auto n : lt2)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;

	cout << endl;
	//删除第二个节点的数据
	list<int>::iterator lit2 = lt2.begin();
	for (int i = 0; i < 1; i++)
	{
		++lit2;
	}
	//出了循环之后,就达到了我们所指定的位置
	lt2.erase(lit2);
	cout << "删除原本的第二个节点的数据:" << endl;
	for (auto n : lt2)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;

	cout << endl;

	//最后clear一下
	lt2.clear();
	cout << "clear:" << endl;
	for (auto n : lt2)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;

}

void testlistOperations()
{
	//那么这一块的就是list里面比较独特的一些函数了
	list<int> lt1;
	for (int i = 1; i <= 8; ++i)
	{
		lt1.push_back(i);//直接尾插就完事了
	}
	cout << "lt1:" << endl;
	for (auto n : lt1)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;
	cout << endl;

	//list里面提供的一个reserve函数
	lt1.reverse();
	cout << "lt1 reverse:" << endl;
	for (auto n : lt1)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;
	cout << endl;

	//remove函数,即将指定的数据给移除掉
	//不是像erase一样去删除指定位置的数据
	//所以是传入要删除的数据,而不是迭代器
	lt1.remove(2);
	cout << "lt1 remove2:" << endl;
	for (auto n : lt1)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;
	cout << endl;

	list<int> lt2;
	lt2.push_back(5);
	lt2.push_back(8);
	lt2.push_back(45);
	lt2.push_back(4);
	lt2.push_back(9);
	lt2.push_back(0);
	lt2.push_back(1);
	lt2.push_back(5);
	lt2.push_back(4);
	lt2.push_back(98);
	lt2.push_back(100);
	cout << "lt2:" << endl;
	for (auto n : lt2)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;
	cout << endl;

	//因为std里面的sort函数不支持传入list的迭代器
	//所以在list中,专门提供了一个sort排序函数
	lt2.sort();
	cout << "lt2 sort:" << endl;
	for (auto n : lt2)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;
	cout << endl;

	//unique函数,即去重函数
	//即将链表中有相同的元素都去掉,只留下一个
	//但是要求是在已经排序后的list才行
	lt2.unique();
	cout << "lt2 unique(去重):" << endl;
	for (auto n : lt2)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;
	cout << endl;

	//merge函数,即将一个list合并进另一个list
	//但是也是要求两个list都是已经排好序的
	//而且合并之后,也是自动有序的
	//但是比较鸡贼的是
	//在merge之后,被合并到另一个list的list
	//是会变为空的
	lt1.sort();
	lt1.merge(lt2);
	cout << "lt1.merge(lt2),把lt2合并到lt1中:" << endl;
	for (auto n : lt1)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;
	//lt1不为空,kt2为空
	cout << "lt2:nullptr";
	cout << endl;
	cout << endl;

	//splice函数,即剪切函数
	//一个挺好用的函数
	//就是把一个list剪切了
	//或者是一个list里面的我们所指定的位置的数据给剪切了
	//注意:被剪切的那一个,就没了
	//然后放在另一个list里面我们所指定的位置(迭代器)
	list<int> lt3;
	for (int i = 10; i <= 15; ++i)
	{
		lt3.push_back(i);//直接尾插就完事了
	}
	auto lt3it = lt3.begin();
	cout << "lt3:" << endl;
	for (auto n : lt3)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;
	list<int> lt4;
	lt4.push_back(85);
	lt4.push_back(24);
	lt4.push_back(56);
	lt4.push_back(45);
	lt4.push_back(55);
	cout << "lt4:" << endl;
	for (auto n : lt4)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;
	cout << endl;
	lt3.splice(lt3it, lt4);
	cout << "将lt4插入到lt3原本的第一个节点前面" << endl;
	//splice之后,lt4里面就没有数据了
	for (auto n : lt3)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;
	cout << "lt4:nullptr" << endl;
	cout << endl;

	list<int> lt5;
	for (int i = 10; i <= 15; ++i)
	{
		lt5.push_back(i);//直接尾插就完事了
	}
	auto lt5it = lt5.begin();
	cout << "lt5:" << endl;
	for (auto n : lt5)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;

	//我们可以将lt5中的某一个迭代器指向的数据给剪切到lt5中另一个迭代器指向的位置
	auto lt5it1 = lt5.begin();
	for (int i = 1; i < 3; ++i)
	{
		++lt5it1;
	}
	cout << endl;
	lt5.splice(lt5it, lt5, lt5it1);
	//第一个参数是要插入的位置迭代器
	//第二个参数是被剪切的list
	//第三个参数是被剪切中的list的要被剪切的数据的位置迭代器
	cout << "将lt5中原本的第3个节点的数据剪切到原本的第2个节点的数据之前:" << endl;
	for (auto n : lt5)
	{
		cout << n << "->";
	}
	cout << "nullptr";
	cout << endl;

}

//测试一下list提供的排序的效率
//和std里面的sort函数比较
void test_op1()
{
	srand(time(0));
	const int N = 1000000;

	list<int> lt1;
	vector<int> v;

	for (int i = 0; i < N; ++i)
	{
		auto e = rand() + i;
		lt1.push_back(e);
		v.push_back(e);
	}

	int begin1 = clock();
	// 排序
	sort(v.begin(), v.end());
	int end1 = clock();

	int begin2 = clock();
	lt1.sort();
	int end2 = clock();

	printf("vector sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
}

//先将要排序的list的数据都丢进vector里面
//然后对vector进行std里面的sort
//排好序后再转移回list里面
void test_op2()
{
	srand(time(0));
	const int N = 1000000;

	list<int> lt1;
	list<int> lt2;

	for (int i = 0; i < N; ++i)
	{
		auto e = rand() + i;
		lt1.push_back(e);
		lt2.push_back(e);
	}

	int begin1 = clock();
	// 拷贝vector
	//vector的迭代器构造函数
	vector<int> v(lt2.begin(), lt2.end());

	// 排序
	sort(v.begin(), v.end());

	// 拷贝回lt2
	lt2.assign(v.begin(), v.end());

	int end1 = clock();

	int begin2 = clock();
	lt1.sort();
	int end2 = clock();

	printf("list copy vector sort copy list sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
}

int main()
{
	testlistconstructor();
	testlistModifiers();
	testlistOperations();
	test_op1();
	test_op2();
	return 0;
}

OK,到这里,就差不多OK了,而一样的,我们接下来几篇博客,就要开始着手模拟实现list1了,在这里提前预告一下,list的模拟实现,可是要比vector和string的模拟实现要难哦,大家做好心理准备。

结语:于链表的指针交错中,读懂 C++ 的设计智慧

当你敲完testlistOperations()函数的最后一个分号,看着控制台中链表元素有序地插入、删除、合并、排序,想必对std::list已经有了全新的认知。从vector的连续内存跳跃到list的非连续节点,这不仅是容器类型的切换,更是对 C++"场景化设计" 思想的深度体悟。

回顾这篇博客的内容,我们从 "为什么需要list" 的灵魂拷问出发,一步步揭开了它的底层面纱:双向链表的节点结构、哨兵节点的巧妙设计、双向迭代器的特性约束,再到insert/erase的 O (1) 高效操作、splice/merge等特有接口的实战用法。每一个知识点的背后,都藏着 STL 设计者对 "效率与灵活" 的极致追求 ------ 用额外的指针开销换取修改操作的高效,用非连续存储摆脱扩容的束缚,用哨兵节点简化边界处理,这正是list区别于vector的核心竞争力。

或许在学习过程中,你曾为 "不能用下标访问" 而困惑,为 "迭代器不能直接加减" 而烦恼,为 "splicemerge的使用场景" 而纠结。但当你亲手写出test_op2()中 "list 转 vector 排序再转回 list" 的对比代码,看着两种排序方式的时间差异,便会豁然开朗:C++ 中没有 "万能容器",只有 "适配场景的容器"。list的每一个 "不完美",都是为了在 "频繁修改" 的场景中做到极致完美。

你一定记得list的迭代器有效性规则 ------ 插入元素后所有迭代器仍有效,删除元素后仅被删节点的迭代器失效。这与vector扩容后迭代器全部失效形成鲜明对比,背后是链表 "节点独立存储" 的结构优势。这种设计让list在频繁插入删除的场景中(如日志系统、任务调度队列)具备天然优势,无需像vector那样频繁重新获取迭代器,大大降低了代码复杂度。

list特有的spilce函数,更是将链表的 "指针操作优势" 发挥到了极致。它无需复制元素,仅通过调整节点的prevnext指针,就能实现元素的快速转移,这种 "剪切 - 粘贴" 的高效操作,是vector永远无法企及的。当你在项目中需要合并两个链表、调整元素顺序时,spilce总能以 O (1) 的时间复杂度给你惊喜,这正是理解底层原理后才能解锁的 "高效技巧"。

在实战示例部分,我们通过testlistconstructortestlistModifierstestlistOperations三个函数,覆盖了list从构造到修改、从特有操作到性能对比的全场景用法。尤其是test_op1test_op2的性能测试,更是直观地告诉我们:没有绝对高效的容器,只有适合场景的选择。当数据量庞大且需要排序时,list自带的sort函数虽然便捷,但效率不如 "转 vector 排序再转回 list"------ 这并非list的缺陷,而是不同数据结构的特性使然。理解这一点,才能真正做到 "因地制宜" 地选择容器。

学习list的过程,也是对 C++ 内存管理和迭代器设计的深度复盘。你会发现,list的节点创建依赖于分配器的allocatedeallocate,元素构造依赖于 placement-new,这与vector的连续内存分配形成鲜明对比;list的双向迭代器仅支持++--操作,这是由链表的非连续存储特性决定的。这些细节看似琐碎,却共同构成了 C++ 容器设计的底层逻辑 ------容器的特性永远由其底层数据结构决定

或许你会觉得list的使用门槛比vector高,需要关注迭代器移动、特有接口的使用场景等细节。但正是这些细节,让你从 "只会调用 API 的使用者" 成长为 "理解设计本质的思考者"。当你能清晰地说出listvector的内存布局差异、迭代器有效性区别、适用场景边界时,你对 C++ 的理解已经迈上了一个新的台阶。

接下来,我们将进入list模拟实现的环节。正如博客末尾预告的那样,list的模拟实现难度远超vectorstring------ 它需要我们手动设计节点结构、实现双向迭代器、处理哨兵节点的边界情况,还要精准复刻insert/erase/splice等接口的底层逻辑。这无疑是一场挑战,但也是一次绝佳的成长机会。通过模拟实现,你将亲手搭建双向链表的骨架,感受指针操作的精妙,彻底吃透list的底层原理,这种 "从 0 到 1" 的构建过程,远比单纯学习接口用法更有价值。

在未来的学习和开发中,希望你能带着今天的收获,灵活运用listvector这两个 "互补品":当需要频繁随机访问、元素数量稳定时,毫不犹豫地选择vector;当需要频繁中间插入删除、元素数量不确定时,果断使用list。这种 "根据场景选择工具" 的思维,是优秀程序员的核心素养之一。

最后,我想对你说:学习 C++ 容器的道路,就像在探索一座庞大的城堡,每一个容器都是一个独特的房间,里面藏着设计的智慧和高效的技巧。list只是这座城堡中的一个房间,接下来还有dequemapset等更多精彩等待你去发现。或许过程中会遇到困惑和挫折,但请相信,每一次深入底层的探索,每一次亲手实践的代码,都是在为你的编程功底添砖加瓦。

请保持这份对底层原理的好奇和探索欲,在 C++ 的世界里继续深耕。当你能从容应对各种容器的选择与使用,当你能亲手模拟实现复杂的数据结构时,你会发现,曾经看似艰难的挑战,都已成为你成长路上最宝贵的财富。

下一篇,我们将正式开启list模拟实现的征程,一起攻克节点设计、迭代器实现、接口复刻等难题。准备好了吗?让我们继续在 C++ 的世界里,探索更多未知,收获更多成长!

相关推荐
UrbanJazzerati2 小时前
PostgreSQL安装与ACID介绍
后端·面试
Hx_Ma162 小时前
测试题(二)
java·开发语言
tackchen2 小时前
venv-manager 管理 Conda 环境 和 Python 虚拟环境 (venv)
开发语言·python·conda
_OP_CHEN2 小时前
【算法提高篇】(三)线段树之维护更多的信息:从基础到进阶的灵活运用
算法·蓝桥杯·线段树·c/c++·区间查询·acm/icpc·信息维护
mjhcsp2 小时前
C++Lyndon 分解超详解析
c++·算法·lyndon
lly2024062 小时前
ASP #include 指令详解
开发语言
2501_936960362 小时前
ROS快速入门杰哥教程1-77残缺版
java·开发语言
Mr_health2 小时前
leetcode:组合排列系列
算法·leetcode·职场和发展
游乐码2 小时前
c#封装成员变量和成员方法和访问修饰符
开发语言·c#