【C++】STL--List容器--拆析解剖List的实现以及List的底层--详解

一. list的介绍及使用

1.1 list的介绍

文档介绍

https://cplusplus.com/reference/list/list/?kw=list

**list 定义:**list 是带头双向循环链表,每个节点存数据 + 前后两个指针,支持前后双向遍历。

list 的优点:list 在任意位置插入和删除元素非常快,时间复杂度是 O(1),因为它只需要改一下前后节点的指针就行,不用像 vector 那样搬动后面的所有元素。而且 list 没有扩容的概念,每次插入新节点就是单独开一块内存,不存在 vector 那种扩容时全部搬移的问题,所以迭代器也不会因为插入而失效,只有被删除的那个节点的迭代器会失效。头尾插入删除也很

快,直接操作头节点和尾节点就行。

list 的缺点:list 最大的问题是不支持随机访问 。想取第 6 个元素,必须从头开始一步一步往后走,时间复杂度是 O(N),不能像 vector 那样直接用下标 v[5] 取出来。所以遍历 list 只能用迭代器,不能写 list[3]。另一个问题是内存开销大。每个节点除了存数据,还要存两个指针(指向前一个和后一个节点),如果数据本身很小(比如只存个 int),指针占的空间反而比数据还大,浪费内存。而 vector 数据是连续存放的,没有这个额外开销。

和 forward_list 对比:forward_list 是单向链表,每个节点只存一个指针(指向下一个节点),比 list 省了一个指针的内存,但只能单向遍历,不支持尾插尾删。不过 forward_list 实际开发中用的非常少,因为单链表尾删效率太低,不实用。

list的使用:如果程序需要频繁在中间位置插入和删除元素,就选 list。如果频繁随机访问,或者元素数量固定,就选 vector。两者各有各的用场,看具体场景选。


1.2list的使用

list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。以下为list中一些常见的重要接口。

1.2.1 list的构造

1. 默认构造

cpp 复制代码
list<int> lt1;   // 构造一个空的 list,里面啥也没有

不传任何参数,造一个空链表。


2. 填充构造

cpp 复制代码
list<int> lt2(5, 10);   // 构造一个 list,里面有 5 个 10
// lt2 = [10, 10, 10, 10, 10]

第一个参数是元素个数,第二个参数是每个元素的值。不传第二个参数的话,值取类型的默认值(比如 int 就是 0)。


3. 拷贝构造

cpp 复制代码
list<int> lt3(lt2);   // 用 lt2 拷贝一份新的 list

拿已有的 list 复制一份出来,内容和原 list 一样,互不影响。


4. 迭代器区间构造

cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
list<int> lt4(v.begin(), v.end());   // 用 vector 的区间构造 list
// lt4 = [1, 2, 3, 4, 5]

只要是迭代器区间,不管来自 vector、list 还是其他容器,都能拿来构造 list。这也体现了 STL 的迭代器接口统一的设计思想,不同容器之间可以互相拷贝数据。


1.2.2 list iterator的使用

此处,大家可暂时将迭代器理解成一个指针,该指针指向list中的某个节点。

list 的迭代器分两种:正向迭代器和反向迭代器。每种又分普通版和 const 版。

常迭代器版本

cpp 复制代码
const_iterator cbegin() const;   // 只能读不能写
const_iterator cend() const;

const_reverse_iterator crbegin() const;
const_reverse_iterator crend() const;

begin() / end()

begin():指向第一个元素

end():指向最后一个元素的下一个位置(哨兵位)

cpp 复制代码
list<int> lt = {1, 2, 3, 4, 5};
list<int>::iterator it = lt.begin();
while (it != lt.end()) {
    cout << *it << " ";   // 输出:1 2 3 4 5
    ++it;
}

rbegin() / rend()

rbegin():反向迭代器,指向最后一个元素(相当于正向的 end() 位置)

rend():反向迭代器,指向第一个元素的前一个位置(相当于正向的 begin() 位置)

cpp 复制代码
list<int>::reverse_iterator rit = lt.rbegin();
while (rit != lt.rend()) {
    cout << *rit << " ";   // 输出:5 4 3 2 1
    ++rit;                 // 注意:反向迭代器++是往前移动
}

功能: 反转 list 中元素的顺序。调用 reverse() 后,list 里的元素会完全倒过来排列。

cpp 复制代码
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
// lt = 1, 2, 3, 4, 5

lt.reverse();
// lt = 5, 4, 3, 2, 1

底层原理

list 是双向链表,反转不需要重新开空间搬数据,只需要把每个节点的 _prev_next 指针交换一下,然后把头节点和尾节点的指向改一下就行了。时间复杂度 O(N),需要遍历所有节点交换指针。


const 迭代器

当对象是 const 类型时,只能调用 const 版本的 begin() 和 end(),返回的是 const_iterator,只能读不能写

cpp 复制代码
void PrintList(const list<int>& l)
{
    list<int>::const_iterator it = l.begin();
    while (it != l.end())
    {
        cout << *it << " ";
        // *it = 10;   // 这句会编译报错,const迭代器不能修改值
        ++it;
    }
}

void TestReverse()
{
    int arr[] = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20};
    list<int> lt(arr, arr + sizeof(arr)/sizeof(arr[0]));
    
    PrintList(lt);   // const迭代器打印:2 4 6 8 10 12 14 16 18 20

    lt.reverse();

    PrintList(lt);   // const迭代器反向打印:20 18 16 14 12 10 8 6 4 2
}

遍历演示:

cpp 复制代码
void PrintList(const list<int>& l)
{
    list<int>::const_iterator it = l.begin();
    while (it != l.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

void TestListTraverse()
{
    // 用数组初始化 list
    int arr[] = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20};
    list<int> lt(arr, arr + sizeof(arr) / sizeof(arr[0]));
    
    // 正向遍历(普通迭代器)
    list<int>::iterator it = lt.begin();
    while (it != lt.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
    
    // 反向遍历
    list<int>::reverse_iterator rit = lt.rbegin();
    while (rit != lt.rend())
    {
        cout << *rit << " ";
        ++rit;
    }
    cout << endl;
    
    // 范围 for 遍历
    for (int x : lt)
    {
        cout << x << " ";
    }
    cout << endl;
}

begin() 第一个元素;

end() 最后一个元素后面;

rbegin() 最后一个元素;

rend() 第一个元素前面。

总结一下这个方向:

迭代器类型 获取方式 能否修改值
iterator l.begin() 可读可写
const_iterator l.cbegin() 或 const 对象 只读
reverse_iterator l.rbegin() 可读可写
const_reverse_iterator l.crbegin() 只读

【注意】

  1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动

  2. rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动


1.2.3 list capacity(容量操作)

https://cplusplus.com/reference/list/list/size/

https://cplusplus.com/reference/list/list/empty/

empty()

检测 list 是否为空,里面没有元素就返回 true,有元素就返回 false。

cpp 复制代码
list<int> lt;
if (lt.empty()) {
    cout << "链表为空" << endl;   //不报错直接执行这句话
}

lt.push_back(10);
if (!lt.empty()) {
    cout << "链表不为空,有 " << lt.size() << " 个元素" << endl;
}

size()

返回 list 中有效节点的个数,也就是当前存了多少个元素。

cpp 复制代码
list<int> lt;
cout << lt.size() << endl;   // 0

lt.push_back(10);
lt.push_back(20);
lt.push_back(30);
cout << lt.size() << endl;   // 3

1.2.4 list element access(访问操作)

https://cplusplus.com/reference/list/list/front/

https://cplusplus.com/reference/list/list/back/

front()

返回第一个元素的引用,可以直接读取或修改。

cpp 复制代码
list<int> lt;
lt.push_back(10);
lt.push_back(20);
lt.push_back(30);

cout << lt.front() << endl;   // 10
lt.front() = 100;             // 修改第一个元素
cout << lt.front() << endl;   // 100

back()

返回最后一个元素的引用,可以直接读取或修改。

cpp 复制代码
list<int> lt;
lt.push_back(10);
lt.push_back(20);
lt.push_back(30);

cout << lt.back() << endl;    // 30
lt.back() = 300;              // 修改最后一个元素
cout << lt.back() << endl;    // 300

const 版本

当 list 是 const 对象时,返回的是 const 引用,只能读不能改。

cpp 复制代码
void PrintFirstAndLast(const list<int>& lt)
{
    cout << lt.front() << endl;   // 只能读
    cout << lt.back() << endl;    // 只能读
    // lt.front() = 100;          // 编译报错,const对象不能修改
}

1.2.5 list modifiers(修改操作)

说明:list 可以在任意位置 进行 O(1)插入删除操作。

1.头尾插入删除操作

cpp 复制代码
void TestPushPop()
{
    list<int> lt;                     // 创建空链表
    lt.push_back(10);                 // 尾部插入10 : 10
    lt.push_back(20);                 // 尾部插入20 :10, 20
    lt.push_back(30);                 // 尾部插入30 :10, 20, 30
    PrintList(lt);                    // 输出:10 20 30

    lt.push_front(5);                 // 头部插入5 : 5, 10, 20, 30
    lt.push_front(0);                 // 头部插入0 : 0, 5, 10, 20, 30
    PrintList(lt);                    // 输出:0 5 10 20 30

    lt.pop_back();                    // 删除尾部元素 :0, 5, 10, 20
    lt.pop_front();                   // 删除头部元素 :5, 10, 20
    PrintList(lt);                    // 输出:5 10 20
}

push_backpop_back 是在链表尾部操作,push_frontpop_front 是在链表头部操作。list 是双向链表,头尾操作都是 O(1),效率高。

2.任意位置插入删除

cpp 复制代码
void TestInsertErase()
{
    list<int> lt;
    for (int i = 1; i <= 5; ++i)
        lt.push_back(i);              // 循环尾插 1 2 3 4 5
    PrintList(lt);                    // 输出:1 2 3 4 5

    auto it = lt.begin();             // it 指向第一个元素 1
    ++it;                             // it 向后走一步,指向第二个元素 2
    lt.insert(it, 10);                // 在 it 前面插入 10
    PrintList(lt);                    // 输出:1 10 2 3 4 5

    lt.erase(it);                     // 删除 it 指向的元素2
    PrintList(lt);                    // 输出:1 10 3 4 5

    lt.erase(lt.begin(), lt.end());   // 删除整个链表中所有元素
    PrintList(lt);                    // 输出:为空
}

insert 在指定位置前面插入元素,erase 删除指定位置的元素。list 的 insert 和 erase 都是 O(1) 复杂度,因为只改指针,不用搬动数据erase(lt.begin(), lt.end()) 是区间删除,把整个链表清空。

3.交换和清空

cpp 复制代码
void TestSwapClear()
{
    list<int> lt1 = {1, 2, 3};        // 创建链表 lt1 = 1, 2, 3
    list<int> lt2 = {9, 8, 7, 6};     // 创建链表 lt2 = 9, 8, 7, 6
    
    PrintList(lt1);                   // 输出:1 2 3
    PrintList(lt2);                   // 输出:9 8 7 6

    lt1.swap(lt2);                    // 交换两个链表的内容
    PrintList(lt1);                   // 输出:9 8 7 6
    PrintList(lt2);                   // 输出:1 2 3

    lt1.clear();                      // 清空 lt1 所有元素
    cout << lt1.size() << endl;       // 输出:0
}

swap 交换两个 list 的内容,只交换头节点指针,时间复杂度 O(1),效率很高。clear 清空链表所有节点,逐个释放节点内存,时间复杂度 O(N)。size() 返回当前元素个数,O(1) 复杂度。

问题补充:

为什么 C++98 建议用容器自己的 swap,而不是算法里的 swap?

算法里的 swap 干了啥

cpp 复制代码
C++98 的 std::swap:

template <class T> 
void swap(T& a, T& b)
{
    T c(a);    // 拷贝构造
    a = b;     // 赋值
    b = c;     // 赋值
}

交换两个对象,需要经历 1 次拷贝构造 + 2 次赋值

vector 用算法 swap 干了啥

假设有两个 vector,各存了1万个数据:

cpp 复制代码
vector<int> v1(10000, 1);
vector<int> v2(10000, 2);
swap(v1, v2);   // 算法里的 swap

执行过程:

T c(a):把 v1 的 1万个数据深拷贝一份,开新空间

a = b:把 v2 的 1万个数据深拷贝给 v1,v1 原来的 1万个数据释放

b = c:把临时对象 c 的 1万个数据深拷贝给 v2,v2 原来的 1万个数据释放

整整深拷贝了 3 次,开辟释放了 3 次内存,1万个元素被搬来搬去 3 趟,代价巨大。


C++98 里算法版 swap 需要深拷贝 3 次,代价很大,而容器自己的 swap 只交换几个指针,几乎零代价,所以 C++98 建议用容器自己的 swap。C++11 引入移动语义后算法版 swap 优化了很多,但容器自己的 swap 仍然是最优选择,因为永远是 O(1) 的指针交换。

1.2.6 list的迭代器失效

前面说过,此处大家可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无
效,即该节点被删除了
。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入
时是不会导致list的迭代器失效的,只有在删除时才会失效
,并且失效的只是指向被删除节点的迭

代器,其他迭代器不会受到影响。

图示:

cpp 复制代码
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())
	{
		// erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给
		其赋值
			l.erase(it);
		++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++);    // it = l.erase(it);
	}
}

vector(连续数组)

  1. insert

会失效

原因分两种情况:

扩容:底层重新分配内存,所有迭代器全部作废

不扩容:插入点之后的元素整体后移,原来那个 pos 指向的位置内容已经变了,即使迭代器本身没坏,语义也失效了

  1. erase

会失效删除点之后的元素整体前移,pos 指向的内容被覆盖,意义不再成立


list(双向链表)

  1. insert

不会失效底层是独立节点,插入只是新增一个节点,原来 pos 指向的那个节点没有任何变化,仍然有效

  1. erase

会失效pos 指向的那个节点被释放了,继续使用就是野指针,必须重新获取有效迭代器

【知识补充】

a.容器迭代器的分类

使用功能角度分:正向迭代器、反向迭代器,以及各自的 const 版本(只读)。

容器底层结构角度分,迭代器分为三大类:

1. 单向迭代器(Unidirectional Iterator)

典型容器:forward_list(单向链表)、unordered_set / unordered_map(哈希表)

特征:只能 ++(向前移动),不支持 --(后退)

含义:只能从前往后单向遍历

2. 双向迭代器(Bidirectional Iterator)

典型容器:list(双向链表)、set / map(红黑树)

特征:既能 ++,也能 --

含义:可以前后双向遍历

3. 随机访问迭代器(Random Access Iterator)

典型容器:stringvectordequearray

特征:支持 ++--+-[] 下标访问,甚至支持 <> 等比较运算

含义:可以直接跳转到任意位置,底层通常是连续内存(deque 略有不同,但接口支持随机访问)

注意:C++ 标准库迭代器类别还有更细的层级(如输入迭代器、输出迭代器),但在日常使用中,上述三类最常用


迭代器的本质

**迭代器是一种抽象接口,它在不暴露容器底层实现细节的前提下,提供统一的方式来访问或修改容器中存储的数据。**简单说:你不需要知道容器是数组、链表还是树,只要拿到迭代器,就能按统一方式遍历和操作。


容器、迭代器、算法之间的关系

容器 负责存储数据;算法 (如 sortfindcopy)负责处理数据;迭代器是两者之间的粘合剂。

三者关系可以用一句话概括:

算法通过迭代器间接访问容器,迭代器屏蔽了容器之间的底层差异,使得同一套算法可以适用于不同容器。举个例子就是: std::find 可以同时用于 vectorlistset,靠的就是迭代器统一了访问方式。

迭代器的五种分类(标准库源码中的继承体系)

C++ 标准库在 stl_iterator.h 中定义了五个迭代器标签(tag),它们之间存在继承关系:

  • input_iterator_tag ------ 只读迭代器(输入迭代器),只能读取元素,单次遍历;
  • output_iterator_tag ------ 只写迭代器(输出迭代器),只能写入元素,单次遍历;
  • forward_iterator_tag ------ 单向迭代器 ,继承自 input_iterator_tag,可多次遍历,支持 ++
  • bidirectional_iterator_tag ------ 双向迭代器 ,继承自 forward_iterator_tag,支持 ++--
  • random_access_iterator_tag ------ 随机访问迭代器 ,继承自 bidirectional_iterator_tag,支持 ++--+-[]、比较运算

b.迭代器两种实现方式的核心逻辑

迭代器的实现方式取决于容器的底层数据结构。不同容器内部内存布局不同,遍历/迭代的细节也就不同,因此迭代器的实现分为两类:


方式一:原生指针天然就是迭代器

适用容器:底层空间物理地址连续的容器,如 stringvectorarray

原因

连续空间意味着元素在内存中一个挨着一个排列;

对原生指针执行 ++,天然移动到下一个元素的位置;

执行 --,天然回到上一个元素的位置;

执行 * 解引用,天然得到该位置存储的数据.

得到的结论

此时原生指针本身就是迭代器。并不是原生指针有多厉害,而是因为它指向的空间物理地址连续,指针运算天然契合了遍历需求。

所以 vector<int>::iterator 实际上就是 int*(或类似原生指针的简单包装)。

方式二:封装类来实现迭代器

适用容器:底层空间物理地址不连续的容器,如 listsetmap

原因

list 的节点散落在内存各处,节点内包含 data(数据)、prev(前驱指针)、next(后继指针),对原生指针执行 ++,只会跳到内存中的下一个地址,那里面可能是垃圾数据,而不是链表的下一个节点对原生指针执行 * 解引用,得到的是整个结构体,而不是单纯的数据。

解决方案

用一个类封装原生指针,重载相关运算符,控制对象使用这些运算符时的行为。这个类的对象用起来像指针一样,但底层逻辑由我们自己定义。

必须重载的运算符(目的是让对象用起来像指针)

operator*() ------ 解引用,获取当前节点存储的数据

operator->() ------ 成员访问,用于访问节点内的成员(如 it->data

operator++()operator++(int) ------ 前置/后置自增,移动到下一个节点

operator==()operator!=() ------ 比较两个迭代器是否相等
按需重载的运算符(取决于容器是否支持双向遍历):

operator--()operator--(int)------list 是双向链表,需要重载;forward_list 是单向链表,不需要

重要的区分点

方式一中的原生指针就是迭代器;方式二中的类对象像指针但不是原生指针

两者在使用形式上完全一致(++--*-> 都支持),但底层实现完全不同:

方式一靠的是内存连续,指针运算自然生效

方式二靠的是运算符重载,手动控制遍历逻辑


1.2.7容器操作

1.sort

https://cplusplus.com/reference/list/list/sort/

sort()

稳定排序:值相同的元素在排序后仍保持原来的先后次序。

只移动节点,不操作元素本身:排序过程只调整链表节点的指针链接,不会对元素对象进行构造、销毁或拷贝。

链表排序效率一般:由于链表不支持随机访问,排序需要较多指针操作,整体效率不如数组或 vector,但在需要频繁增删元素的场景中仍有其适用价值。

默认按升序排列(使用 <);若要降序,需传入比较函数对象,例如 greater<int>()

写法一:使用函数对象(具名对象)

cpp 复制代码
include <list>
#include <functional>
using namespace std;

void test()
{
    int arr[] = {10, 2, 5};
    list<int> lt(arr, arr + sizeof(arr) / sizeof(int));

    greater<int> gt;
    lt.sort(gt);   // 降序:10 5 2
}

写法二:使用匿名对象

cpp 复制代码
#include <list>
#include <functional>
using namespace std;

void test()
{
    int arr[] = {10, 2, 5};
    list<int> lt(arr, arr + sizeof(arr) / sizeof(int));

    lt.sort(greater<int>());   // 降序:10 5 2
}

2.unique

https://cplusplus.com/reference/list/list/unique/

函数作用

移除容器中连续相等的元素,只保留每组连续相等元素中的第一个。

注意:不是全局去重,只针对相邻元素。

cpp 复制代码
list<int> lt = {1, 1, 2, 2, 3, 1, 1};
lt.unique();
// 结果:1, 2, 3, 1
// 相同元素不相邻时不会被删除

3.remove

https://cplusplus.com/reference/list/list/remove/

函数作用

移除容器中所有值与 val 相等的元素,而不是只删除第一个。

cpp 复制代码
#include <list>
using namespace std;

void test()
{
    int arr[] = {5, 8, 2, 2, 9};
    list<int> lt(arr, arr + sizeof(arr) / sizeof(int));

    lt.remove(2);   // 删除所有值为 2 的元素
    // 结果:5 8 9
}

注意: remove() 不会对容器进行排序,只遍历删除匹配值;

如果容器中有多个相同值,全部会被移除;

4. reverse

https://cplusplus.com/reference/list/list/reverse/

函数作用

反转容器中元素的顺序,将第一个变为最后一个,最后一个变为第一个。

cpp 复制代码
#include <list>
using namespace std;

void test()
{
    int arr[] = {2, 4, 6, 8, 10};
    list<int> lt(arr, arr + sizeof(arr) / sizeof(int));

    lt.reverse();   // 反转顺序
    // 结果:10 8 6 4 2
}

5. merge

cplusplus.com/reference/list/list/merge/

函数作用

将两个已排序的链表合并为一个有序链表,合并后目标链表仍保持有序。

注意前提 :合并前两个 list 容器都必须是已排序的。

cpp 复制代码
#include <list>
using namespace std;

void test()
{
    int arr1[] = {1, 3, 5};
    int arr2[] = {2, 4, 6};
    list<int> lt1(arr1, arr1 + sizeof(arr1) / sizeof(int));
    list<int> lt2(arr2, arr2 + sizeof(arr2) / sizeof(int));

    // lt1 和 lt2 都已排序
    lt1.merge(lt2);   // 将 lt2 合并到 lt1 中
    // lt1: 1 2 3 4 5 6
    // lt2:空
}

6.emplace

https://cplusplus.com/reference/list/list/emplace

函数作用

在指定位置直接构造元素,而不是先创建对象再拷贝或移动插入。

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

struct Student
{
    int id;
    string name;
    
    Student(int i, string n) : id(i), name(n) {}
};

void test()
{
    list<Student> lt;
    
    // 在链表末尾直接构造 Student 对象
    lt.emplace(lt.end(), 0001, "孙悟空");
    // 相当于:lt.insert(lt.end(), Student(0001, "孙悟空")); 但少了临时对象
}

7.emplace_back

https://cplusplus.com/reference/list/list/emplace_back/

函数作用

在链表末尾直接构造元素,而不是先创建对象再拷贝或移动插入。

cpp 复制代码
#include <list>
#include <iostream>
#include <string>
using namespace std;

struct Student
{
    int id;
    string name;
    
    Student(int i, string n) : id(i), name(n) {}
};

void test()
{
    list<Student> lt;
    
    // 在链表末尾直接构造 Student 对象
    lt.emplace_back(0001, "孙悟空");
    lt.emplace_back(0002, "猪八戒");
    // 相当于:lt.push_back(Student(0001, "孙悟空")); 但少了临时对象
}

对比:push_back() 是先造好再搬进去;emplace_back() 是直接在屋里造,不用搬。

lt.push_back(Student(0001, "孙悟空")),那个 Student(0001, "孙悟空") 就是临时对象,用完就销毁了。而 lt.emplace_back(0001, "孙悟空") 直接在链表里造对象,没有临时对象产生


8.emplace_front

https://cplusplus.com/reference/list/list/emplace_front/

函数作用

在链表开头直接构造元素 ,有效增加容器大小(size 加 1),类比 push_front,后者需要拷贝或移动一个已存在的对象。

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

int main()
{
    list<pair<int, char>> mylist;

    // 在链表开头直接构造 pair 对象
    mylist.emplace_front(10, 'a');
    mylist.emplace_front(20, 'b');
    mylist.emplace_front(30, 'c');

    // 输出:mylist contains: (30,c) (20,b) (10,a)
    // 注意:最先插入的在最后,因为每次都在开头插入
    for (auto& x : mylist)
        cout << " (" << x.first << "," << x.second << ")";
    cout << endl;

    return 0;
}

二.List的底层结构

2.1List底层结构

head->next 指向第一个数据节点 (d1);

head->prev 指向最后一个数据节点 (d3);

空链表时,head->nexthead->prev 都指向 head 自身。

2.2STL_list 源码

cpp 复制代码
// stl_list.h(SGI STL)
template <class T>
struct __list_node {
    typedef void* void_pointer;
    void_pointer next;
    void_pointer prev;
    T data;
};

template<class T, class Ref, class Ptr>
struct __list_iterator {
    typedef __list_iterator<T, T&, T*>             iterator;
    typedef __list_iterator<T, const T&, const T*> const_iterator;
    typedef __list_iterator<T, Ref, Ptr>           self;

    typedef Ptr  pointer;
    typedef Ref  reference;
    typedef __list_node<T>* link_type;

    link_type node;  // 核心指针

    reference operator*() const { return node->data; }
    pointer   operator->() const { return &(operator*()); }

    self& operator++() { node = (link_type)(node->next); return *this; }
    self  operator++(int) { self tmp = *this; ++*this; return tmp; }
    self& operator--() { node = (link_type)(node->prev); return *this; }
    self  operator--(int) { self tmp = *this; --*this; return tmp; }

    bool operator==(const self& x) const { return node == x.node; }
    bool operator!=(const self& x) const { return node != x.node; }
};

template <class T, class Alloc = alloc>
class list {
protected:
    typedef __list_node<T> list_node;
    typedef list_node* link_type;

    link_type node;  // 哨兵节点指针

public:
    typedef __list_iterator<T, T&, T*>             iterator;
    typedef __list_iterator<T, const T&, const T*> const_iterator;

    // 构造函数
    list() { empty_initialize(); }

protected:
    void empty_initialize() {
        node = get_node();
        node->next = node;
        node->prev = node;
    }
    // ... 其他构造、析构、插入、删除等实现
};

三.List的模拟实现

3.1list 的节点结构

cpp 复制代码
namespace bit
{
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		list_node(const T& data = T())
			:_data(data)
			,_next(nullptr)
			,_prev(nullptr)
		{}
	};

3.2 list 的迭代器

写法一:用一个模板

cpp 复制代码
// 写法一:一个类模板,通过 Ref 和 Ptr 参数区分普通/const 迭代器
// 优点:代码复用,维护成本低,是 STL 源码中的标准做法

// 模板参数说明:
// T   : 元素类型
// Ref : 引用类型,实例化时传入 T& 或 const T&
// Ptr : 指针类型,实例化时传入 T* 或 const T*
template<class T, class Ref, class Ptr>
struct list_iterator
{
    // 类型别名 
    typedef list_node<T> Node;                 // 节点类型
    typedef list_iterator<T, Ref, Ptr> Self;   // 自身类型

    //  成员变量
    Node* _node;  // 封装节点指针,指向当前迭代器所对应的节点

    //构造函数 
    // 用节点指针构造迭代器
    list_iterator(Node* node)
        : _node(node)
    {}

    // 必须重载的运算符(模拟指针行为) 

    // 1. 解引用 operator*()
    // 功能:返回当前节点数据的引用
    // 返回类型:Ref 可能是 T&(可修改)或 const T&(只读)
    Ref operator*()
    {
        return _node->_data;
    }

    // 2. 成员访问 operator->()
    // 功能:返回当前节点数据的指针,支持 it->member 写法
    // 返回类型:Ptr 可能是 T*(可修改)或 const T*(只读)
    Ptr operator->()
    {
        return &_node->_data;
    }

    // 3. 前置自增 operator++()
    // 功能:迭代器指向下一个节点,返回自增后的自身
    // 典型用法:++it
    Self& operator++()
    {
        _node = _node->_next;  // 指针后移到下一个节点
        return *this;          // 返回自身引用
    }

    // 4. 前置自减 operator--()
    // 功能:迭代器指向前一个节点,返回自减后的自身
    // 典型用法:--it
    Self& operator--()
    {
        _node = _node->_prev;  // 指针前移到上一个节点
        return *this;          // 返回自身引用
    }

    // 5. 后置自增 operator++(int)
    // 功能:返回自增前的旧迭代器,然后让迭代器指向下一个节点
    // 典型用法:it++  (int 参数是占位符,用于区分前置/后置)
    Self operator++(int)
    {
        Self tmp(*this);       // 保存当前状态的副本(旧值)
        _node = _node->_next;  // 指针后移
        return tmp;            // 返回旧值
    }

    // 6. 后置自减 operator--(int)
    // 功能:返回自减前的旧迭代器,然后让迭代器指向前一个节点
    // 典型用法:it--
    Self operator--(int)
    {
        Self tmp(*this);       // 保存当前状态的副本(旧值)
        _node = _node->_prev;  // 指针前移
        return tmp;            // 返回旧值
    }

    // 7. 不等运算符 operator!=()
    // 功能:比较两个迭代器是否不相等(即是否指向不同节点)
    bool operator!=(const Self& s) const
    {
        return _node != s._node;  // 比较节点指针
    }

    // 8. 相等运算符 operator==()
    // 功能:比较两个迭代器是否相等(即是否指向同一个节点)
    bool operator==(const Self& s) const
    {
        return _node == s._node;  // 比较节点指针
    }
};

// 在 list 类中的使用方式 
/*
template <class T>
class list {
public:
    // 通过传不同模板参数,同一个类模板实例化成两种迭代器
    typedef list_iterator<T, T&, T*>             iterator;      // 普通迭代器:可修改元素
    typedef list_iterator<T, const T&, const T*> const_iterator; // const 迭代器:只读

    iterator begin() { return iterator(_head->_next); }
    const_iterator begin() const { return const_iterator(_head->_next); }

    iterator end() { return iterator(_head); }
    const_iterator end() const { return const_iterator(_head); }
};
*/

写法二:写两个类(直观但冗余)

cpp 复制代码
// 写法二:分别写普通迭代器和 const 迭代器两个独立的类
// 缺点:两份代码几乎完全重复,维护成本高

// 普通迭代器(可修改元素) 
template<class T>
struct list_iterator
{
    // 类型别名
    typedef list_node<T> Node;
    typedef list_iterator<T> Self;

    // 成员变量
    Node* _node;  // 封装节点指针

    // 构造函数
    list_iterator(Node* node)
        : _node(node)
    {}

    //重载运算符

    // 1. 解引用:返回 T&,允许修改数据
    T& operator*()
    {
        return _node->_data;
    }

    // 2. 成员访问:返回 T*,允许修改数据
    T* operator->()
    {
        return &_node->_data;
    }

    // 3. 前置++:返回自增后的自身
    Self& operator++()
    {
        _node = _node->_next;
        return *this;
    }

    // 4. 前置--:返回自减后的自身
    Self& operator--()
    {
        _node = _node->_prev;
        return *this;
    }

    // 5. 后置++:返回旧值,再自增
    Self operator++(int)
    {
        Self tmp(*this);
        _node = _node->_next;
        return tmp;
    }

    // 6. 后置--:返回旧值,再自减
    Self operator--(int)
    {
        Self tmp(*this);
        _node = _node->_prev;
        return tmp;
    }

    // 7. 不等运算符
    bool operator!=(const Self& s) const
    {
        return _node != s._node;
    }

    // 8. 相等运算符
    bool operator==(const Self& s) const
    {
        return _node == s._node;
    }
};


//  const 迭代器(只读,不可修改元素) 
template<class T>
struct list_const_iterator
{
    //类型别名
    typedef list_node<T> Node;
    typedef list_const_iterator<T> Self;

    // 成员变量 
    Node* _node;  // 封装节点指针

    // 构造函数
    list_const_iterator(Node* node)
        : _node(node)
    {}

    //  重载运算符

    // 1. 解引用:返回 const T&,只读不可修改
    const T& operator*()
    {
        return _node->_data;
    }

    // 2. 成员访问:返回 const T*,只读不可修改
    const T* operator->()
    {
        return &_node->_data;
    }

    // 3. 前置++:返回自增后的自身
    Self& operator++()
    {
        _node = _node->_next;
        return *this;
    }

    // 4. 前置--:返回自减后的自身
    Self& operator--()
    {
        _node = _node->_prev;
        return *this;
    }

    // 5. 后置++:返回旧值,再自增
    Self operator++(int)
    {
        Self tmp(*this);
        _node = _node->_next;
        return tmp;
    }

    // 6. 后置--:返回旧值,再自减
    Self operator--(int)
    {
        Self tmp(*this);
        _node = _node->_prev;
        return tmp;
    }

    // 7. 不等运算符
    bool operator!=(const Self& s) const
    {
        return _node != s._node;
    }

    // 8. 相等运算符
    bool operator==(const Self& s) const
    {
        return _node == s._node;
    }
};

//  在 list 类中的使用方式
/*
template <class T>
class list {
public:
    typedef list_iterator<T> iterator;
    typedef list_const_iterator<T> const_iterator;
    // ...
};
*/

迭代器的两种实现方式

迭代器说白了就是让遍历容器的方式统一,具体怎么实现看容器本身怎么存数据:

直接用原生指针:像 vector、string 这种一段连续的空间,原生指针自己就能往前走、往后走,拿来就能当迭代器用。

把指针包一层:像 list 这种节点散落在各处的,指针不能直接 ++ 就到下一个节点,得写个类把节点指针包起来,然后重载各种运算符,让这个类的对象用起来跟指针一模一样。

想让这个类像指针,必须重载这些运算符

既然要模仿指针,指针能干啥,这个类就得能干啥:

operator*():解引用,拿到当前指向的数据。

operator->():取成员,支持 it->xxx 这种写法。

operator++()operator++(int):往前走一步,前置后置都得有。

operator==()operator!=():判断两个迭代器是否指向同一个位置,遍历结束要靠它。

有些运算符看情况重载

operator--()operator--(int):只有能往回走的容器才需要,比如 list 是双向链表,可以往前,就需要;forward_list 是单向链表,只能往前,就不需要。

如果是 vector 这种支持随机访问的,还得额外重载 operator+=operator[] 这些。

普通迭代器和 const 迭代器怎么实现

推荐做法 :写一个类模板,多加两个模板参数(引用类型和指针类型)。普通迭代器传 T&T*,const 迭代器传 const T&const T*,一套代码两套用法,省事好维护。

不推荐做法:普通迭代器写一个类,const 迭代器再单独写一个类,代码基本一样,改个 bug 得改两份,自己给自己找麻烦。


1.为什么 list 的 const 迭代器实现起来和 vector 不一样

vector 和 string 的迭代器就是原生指针,想让它只读,直接在前面加个 const 就行,比如 const int*int* 的区别。

但 list 的迭代器是一个类,不是指针。想让这个类变成只读的,关键就是让用户通过它拿到的数据不能被修改。也就是说,operator*() 返回的是常引用,operator->() 返回的是常指针,用户就只能看不能改了。


二、那怎么实现两种迭代器呢?

法一:写两个独立的类

一个普通迭代器类,一个 const 迭代器类。后者把前者的返回值全部改成 const 版本。

这么做当然能跑,但问题是两个类的代码几乎一样,就返回值那里差个 const。以后要加功能或者修 bug,得同时改两份,自己给自己找事做。

法二:用一个模板,靠参数控制返回值类型

既然两个类只有返回值类型不同,那就把返回值类型当成模板参数传进去。

迭代器模板多搞两个参数:一个管引用类型,一个管指针类型。普通迭代器传 T&T*,const 迭代器传 const T&const T*。一套代码,传不同的参数就生成不同的迭代器,省心省力。

3.为什么不能靠函数重载解决这个问题

有人可能会想,在一个类里写两个 operator*(),一个返回 T&,一个返回 const T&,编译器不就能区分了吗?

不能。C++ 规定函数重载只看参数列表,不看返回值。两个函数参数一样、返回值不同,编译器根本分不清该调哪个,直接报错。

4.SGI 源码的做法总结

SGI 全称 Silicon Graphics International (硅谷图形国际公司)。

在 C++ 领域提到 SGI,通常指 SGI STL------该公司实现的经典标准模板库版本,由 Stepanov 和 Lee 等人主导开发,代码质量高、风格清晰,是很多编译器标准库的参考蓝本。
**SGI 就是用了法二,**给迭代器模板加了 RefPtr 两个参数。Ref 决定 operator*() 的返回类型,Ptr 决定 operator->() 的返回类型。

在 list 里面定义两种迭代器的时候,传不同的参数就完事了:

普通迭代器:Ref = T&Ptr = T*

const 迭代器:Ref = const T&Ptr = const T*

cpp 复制代码
// 迭代器模板:Ref 和 Ptr 控制返回值类型
template<class T, class Ref, class Ptr>
struct list_iterator {
    typedef list_node<T> Node;
    typedef list_iterator<T, Ref, Ptr> Self;
    
    Node* _node;
    
    list_iterator(Node* node) : _node(node) {}
    
    // Ref 可能是 T& 或 const T&
    Ref operator*() {
        return _node->_data;
    }
    
    // Ptr 可能是 T* 或 const T*
    Ptr operator->() {
        return &_node->_data;
    }
    
    Self& operator++() {
        _node = _node->_next;
        return *this;
    }
    
    // 其他运算符...
};

// list 类中定义两种迭代器
template<class T>
class list {
public:
    // 普通迭代器:传 T& 和 T*
    typedef list_iterator<T, T&, T*> iterator;
    
    // const 迭代器:传 const T& 和 const T*
    typedef list_iterator<T, const T&, const T*> const_iterator;
    
    iterator begin() {
        return iterator(_head->_next);
    }
    
    const_iterator begin() const {
        return const_iterator(_head->_next);
    }
    
    iterator end() {
        return iterator(_head);
    }
    
    const_iterator end() const {
        return const_iterator(_head);
    }
};

1.遍历 list 的两种场景

用迭代器遍历 list 的时候,不管里面存的是什么类型,方式都一样:从 begin() 走到 end(),逐个访问。

存的是内置类型(int、double 等)*it 直接拿到节点里存的那个值,打印或者赋值都行。

cpp 复制代码
int arr[] = {1, 2, 3, 4, 5};
list<int> lt(arr, arr + sizeof(arr) / sizeof(int));

list<int>::iterator it = lt.begin();
while (it != lt.end())
{
    cout << *it << " ";   // *it 拿到节点里存的数据
    ++it;
}
// 输出:1 2 3 4 5

存的是自定义类型或结构体*it 拿到的是整个对象,想访问里面的成员就得用 ->。比如节点里存的是 TreeNode,想拿 _val 就写 it->_val

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

// ---------- 自定义类型:AA 结构体 ----------
struct AA
{
    int _a1;
    int _a2;
    
    // 构造函数
    AA(int a1 = 0, int a2 = 0)
        : _a1(a1)
        , _a2(a2)
    {}
};

// ---------- 打印容器内容的函数模板 ----------
template<typename Container>
void print_container(const Container& con)
{
    for (auto e : con)
    {
        cout << e << " ";
    }
    cout << endl;
}

// ---------- 测试函数 ----------
void test_list1()
{
    // ========== 第一部分:list 存内置类型 ==========
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);

    // 用迭代器遍历并修改数据
    list<int>::iterator it = lt.begin();
    while (it != lt.end())
    {
        *it += 10;              // 通过解引用修改数据
        cout << *it << " ";     // 打印修改后的值
        ++it;
    }
    cout << endl;

    // 用范围 for 遍历(只读)
    for (auto e : lt)
    {
        cout << e << " ";
    }
    cout << endl;

    // 用打印函数遍历
    print_container(lt);


    //第二部分:list 存自定义类型 AA 
    list<AA> lta;
    lta.push_back(AA(1, 2));     // 插入 AA(1, 2)
    lta.push_back(AA(3, 4));     // 插入 AA(3, 4)
    lta.push_back(AA(5, 6));     // 插入 AA(5, 6)
    lta.push_back(AA(7, 8));     // 插入 AA(7, 8)

    // 用迭代器遍历自定义类型
    list<AA>::iterator ita = lta.begin();
    while (ita != lta.end())
    {
        //方式1:先解引用再 . 访问 
        cout << (*ita)._a1 << ":" << (*ita)._a2 << endl;
        
        // 方式2:用 -> 直接访问(推荐) 
        // 底层:ita.operator->()->_a1
        // 编译器特殊处理,本来应该是两个 ->,为了可读性省略了一个
        cout << ita->_a1 << ":" << ita->_a2 << endl;
        
        //  方式3:完整展开形式(了解底层) 
        cout << ita.operator->()->_a1 << ":" << ita.operator->()->_a2 << endl;

        ++ita;
    }
    cout << endl;
}

关于 it->_a1这个写法,底层实际上是 it.operator->()->_a1,也就是迭代器重载的 -> 返回了节点数据的地址,然后这个地址再去访问成员。编译器为了可读性把两个 -> 缩成了一个,写起来才那么简洁。

2.迭代器需不需要自己写拷贝构造、赋值重载、析构?

不需要。

迭代器里虽然包了一个节点指针,但这个指针指向的节点是链表管的,不是迭代器管的。链表创建节点、释放节点都是 list 自己做的事,迭代器只负责指着某个节点,不负责节点的生和死。编译器默认生成的拷贝构造和析构就够用了,不用自己动手

3.vector 迭代器和 list 迭代器有什么区别?

物理上没啥区别,都是 4 个字节(32 位下),里面存的就是一个指针。

cpp

cpp 复制代码
// vector 迭代器 - 原生指针
typedef T* iterator;        // 大小:4 字节(32位)

// list 迭代器 - 封装的类
template<class T, class Ref, class Ptr>
struct list_iterator {
    Node* _node;            // 大小:4 字节(32位)
    // ... 运算符重载
};

区别在于逻辑上:

vector 迭代器 就是个原生指针,++ 就是指针自己往后走一步,因为 vector 内存是连续的,往前走一步就到下一个元素了。

cpp 复制代码
// vector 迭代器的 ++
int* it = ...;
++it;   // 指针直接 +1,跳到下一个元素

list 迭代器 是个类,++ 调的是 operator++() 函数,函数里面把指针从当前节点移到下一个节点。因为 list 内存不连续,不能直接往后走,得靠 _next 指针跳过去。

cpp 复制代码
// list 迭代器的 ++
list_iterator it = ...;
++it;   // 调用 it.operator++(),内部执行 _node = _node->_next

物理上看都是存了一个指针,但执行 ++ 的时候,一个是直接加,一个是调用函数跳转,逻辑完全不一样

3.3list 类框架

cpp 复制代码
namespace bit
{
    template<class T>
    class list
    {
        typedef list_node<T> Node;   // 节点类型别名

    public:
        // 迭代器类型定义(嵌套类型)
        typedef list_iterator<T, T&, T*> iterator;                  // 普通迭代器
        typedef list_iterator<T, const T&, const T*> const_iterator; // const 迭代器

        // 迭代器接口
        iterator begin() { return _head->_next; }
        iterator end() { return _head; }
        const_iterator begin() const { return _head->_next; }
        const_iterator end() const { return _head; }

    private:
        Node* _head;    // 哨兵节点指针
        size_t _size;   // 有效元素个数
    };
}

3.4list 的底层结构

list 的底层是带头双向循环链表。

命名空间与类结构

cpp 复制代码
namespace hjq
{
    // 带头双向循环链表
    template<class T> // T: 节点中数据的类型
    class list
    {
    public:
        // 节点类型别名
        typedef __list_node<T> Node;

        // 迭代器类型定义
        // iterator是内嵌类型,在list类域里面定义的类型(类中类)
        // 所有容器迭代器都叫iterator,用起来统一规范
        
        // 普通迭代器:传 T, T&, T*
        typedef __list_iterator<T, T&, T*> iterator;
        
        // const迭代器:传 T, const T&, const T*
        typedef __list_iterator<T, const T&, const T*> const_iterator;

        // 迭代器接口
        iterator begin();             // 返回指向第一个有效节点的迭代器
        iterator end();               // 返回指向头节点(哨兵)的迭代器
        const_iterator begin() const; // const版本
        const_iterator end() const;   // const版本

        // 默认成员函数 
        list();                                    // 默认构造函数
        template<class InputIterator>
        list(InputIterator first, InputIterator last); // 迭代器区间构造
        list(const list<T>& lt);                   // 拷贝构造(深拷贝)
        list<T>& operator=(list<T> lt);            // 赋值重载(现代写法)
        ~list();                                   // 析构函数

        //容量操作 
        size_t size() const;    // 有效元素个数
        bool empty() const;     // 判空

        // 修改操作 
        iterator insert(iterator pos, const T& data); // 在pos前插入
        iterator erase(iterator pos);                 // 删除pos位置元素
        void push_back(const T& data);                // 尾插
        void push_front(const T& data);               // 头插
        void pop_back();                              // 尾删
        void pop_front();                             // 头删
        void clear();                                 // 清空所有有效元素

    private:
        Node* _head; // 哨兵节点指针(不存数据,指向自己表示空链表)
    };
}

辅助结构(需补充定定义库里面没有的)

cpp 复制代码
// 节点结构
template<class T>
struct __list_node
{
    __list_node<T>* _next; // 指向下一个节点
    __list_node<T>* _prev; // 指向前一个节点
    T _data;               // 存储的数据

    __list_node(const T& data = T())
        : _next(nullptr)
        , _prev(nullptr)
        , _data(data)
    {}
};

// 迭代器结构
template<class T, class Ref, class Ptr>
struct __list_iterator
{
    typedef __list_node<T> Node;
    typedef __list_iterator<T, Ref, Ptr> Self;

    Node* _node; // 封装的节点指针

    __list_iterator(Node* node = nullptr);

    Ref operator*() const;
    Ptr operator->() const;
    Self& operator++();
    Self operator++(int);
    Self& operator--();
    Self operator--(int);
    bool operator==(const Self& s) const;
    bool operator!=(const Self& s) const;
};

1.迭代器类型定义的代码

cpp 复制代码
namespace hjq
{
    template<class T>
    class list
    {
    public:
        // 普通迭代器:传 T, T&, T*
        typedef __list_iterator<T, T&, T*> iterator;
        
        // const迭代器:传 T, const T&, const T*
        typedef __list_iterator<T, const T&, const T*> const_iterator;
    };
}

2.为什么要 typedef 成 iterator 和 const_iterator

__list_iterator<T, T&, T*>__list_iterator<T, const T&, const T*> 是定义在 list 类内部的嵌套类型,用 typedef 重命名为 iteratorconst_iterator,目的很明确:统一规范迭代器的名称

所有容器的迭代器都叫 iterator,不管是 vector、list 还是 map,使用者拿到容器就知道用 ::iterator 来声明迭代器,不用记各种乱七八糟的名字。如果每个容器都各起各的名字,比如 vector 叫 vec_iter,list 叫 list_iter,那用起来成本就太高了。

3.迭代器类型和容器类型的关系

比如: list<Book> lt;(Book自定义的结构体),编译器会这样处理:

第一步:list 类模板根据 Book 实例化

模板参数 T 被替换成 Book

类里面所有的 T 都变成 Book

第二步:迭代器类型自动推导

iterator 的定义是 __list_iterator<T, T&, T*>TBook,所以普通迭代器就变成 __list_iterator<Book, Book&, Book*>

const_iterator 的定义是 __list_iterator<T, const T&, const T*>TBook,所以 const 迭代器就变成 __list_iterator<Book, const Book&, const Book*>

第三步:两个迭代器模板各自实例化

普通迭代器的 RefBook&operator*() 返回 Book&,可以修改书籍数据

const 迭代器的 Refconst Book&operator*() 返回 const Book&,只能读取不能修改

为什么说迭代器和容器是共生的?

其中 TT&T* 都是类型参数。当 list 的类型确定了,TT&T* 的类型自动确定,那么 iterator 的类型同时也自动确定了。

这看起来就像 iteratorlist 是共生的一样------有什么样的容器,就有什么样的迭代器list<Book> 配的就是操作 Book 数据的迭代器,list<int> 配的就是操作 int 数据的迭代器,就像什么样的锁配什么样的钥匙,锁定了,钥匙也跟着确定了。

代码如下看效果:

cpp 复制代码
struct Book
{
    string _name;
    double _price;
};

list<Book> books;
list<Book>::iterator it = books.begin();
list<Book>::const_iterator cit = books.begin();

it->_price = 99.9;      // 可以修改,因为 operator*() 返回 Book&
cit->_price = 99.9;     // 编译报错,因为 operator*() 返回 const Book&

cout << it->_name;      // 可以读取
cout << cit->_name;     // 可以读取

3.4list 的一些成员函数

(1)默认成员函数详解

a. 构造函数

1. 默认构造函数:创建空链表

cpp 复制代码
// 初始化哨兵节点,让 _next 和 _prev 都指向自己(空链表状态)
void empty_init()
{
    _head = new Node;          // 创建哨兵节点(不存数据)
    _head->_next = _head;      // 自己指向自己
    _head->_prev = _head;
    _size = 0;
}

list() { empty_init(); }

2. 初始值列表构造函数(C++11)

cpp 复制代码
list(initializer_list<T> il)
{
    empty_init();              // 先初始化空链表
    for (auto& e : il)         // 遍历初始值列表,逐个尾插
    {
        push_back(e);
    }
}

b. 拷贝构造函数(深拷贝)
cpp 复制代码
// lt2(lt1)  -- 用 lt1 拷贝构造 lt2
list(const list<T>& lt)
{
    empty_init();              // 先初始化空链表

    for (auto& e : lt)         // 遍历 lt 中的每个元素
    {
        push_back(e);          // 逐个尾插到新链表
    }
}

大致过程

创建空链表(只生成哨兵节点)

遍历源链表,每个元素调用 push_back 插入新节点

每个新节点独立分配内存,深拷贝数据


c. 赋值运算符重载(传统写法)
cpp 复制代码
// 传统写法:lt1 = lt3
list<T>& operator=(const list<T>& lt)
{
    // 检查自赋值
    if (this != &lt)
    {
        clear();                    // 清空当前所有有效节点
        for (auto& e : lt)          // 遍历源链表
        {
            push_back(e);           // 逐个尾插
        }
    }
    return *this;
}

缺点 :先 clear() 再插入,期间链表为空,如果插入过程中抛出异常,链表状态被破坏,异常安全性较差


d. 赋值运算符重载(现代写法)
cpp 复制代码
// 现代写法:lt1 = lt3
list<T>& operator=(list<T> lt)   // 传值(拷贝构造一份临时对象)
{
    swap(lt);                    // 交换当前对象和临时对象
    return *this;                // 临时对象出函数作用域自动销毁
}

// swap 交换函数
void swap(list<T>& lt)
{
    std::swap(_head, lt._head);  // 交换哨兵节点指针
    std::swap(_size, lt._size);  // 交换大小
}

大致过程

传值调用,实参拷贝构造出临时对象 lt(复用拷贝构造);

交换当前对象和临时对象的内部资源(_head_size);

函数返回,临时对象销毁,带走原来的旧资源。
优点异常安全,代码简洁,复用拷贝构造,这是 C++ 推荐的现代写法


e. 析构函数
cpp 复制代码
~list()
{
    clear();          // 清空所有有效节点
    delete _head;     // 释放哨兵节点
    _head = nullptr;  // 防止野指针
}

大致流程

clear() 遍历释放所有有效数据节点;

delete _head 释放哨兵节点;

_head 置空,防止后续误用。


(2)容量操作

cpp 复制代码
// 返回有效元素个数
size_t size() const
{
    return _size;
}

// 判空:有效元素是否为 0
bool empty() const
{
    return _size == 0;
}

(3)修改操作

1. insert:在 pos 位置之前插入新节点

cpp 复制代码
iterator insert(iterator pos, const T& x)
        {
            Node* cur = pos._node;       // 当前节点
            Node* prev = cur->_prev;     // 前一个节点
            Node* newnode = new Node(x); // 创建新节点
            // prev <-> newnode <-> cur
            newnode->_next = cur;
            cur->_prev = newnode;
            newnode->_prev = prev;
            prev->_next = newnode;
            ++_size;                     // 维护大小
            return newnode;              // 返回指向新节点的迭代器
        }

pos 位置之前 插入新节点,新节点的值为 x

插入后新节点成为 pos 的前

返回指向新节点的迭代器

时间复杂度 O(1)


2. erase:删除 pos 位置的节点

cpp 复制代码
  iterator erase(iterator pos)
        {
            assert(pos != end());        // 不能删除哨兵节点
            Node* prev = pos._node->_prev;
            Node* next = pos._node->_next;
            // prev <-> next
            prev->_next = next;
            next->_prev = prev;
            delete pos._node;            // 释放被删除节点
            --_size;                     // 维护大小
            return next;                 // 返回指向下一个节点的迭代器
        }

删除 pos 位置的节点

pos 不能是 end()(哨兵节点不能删)

返回指向被删除节点下一个节点的迭代器

时间复杂度 O(1)


push_back:尾插

cpp 复制代码
void push_back(const T& x)
{
    insert(end(), x);            // 在哨兵节点之前插入(即最后一个有效节点之后)
}

end() 指向哨兵节点,在哨兵节点之前插入,就是在链表尾部插入新节点。


push_front:头插

cpp 复制代码
void push_front(const T& x)
{
    insert(begin(), x);          // 在第一个有效节点之前插入
}

begin() 指向第一个有效节点,在它之前插入,新节点就成为新的第一个有效节点。


pop_back:尾删

cpp 复制代码
void pop_back()
{
    erase(--end());              // 删除最后一个有效节点
}

end() 指向哨兵节点,--end() 向前移动一步,指向最后一个有效节点,然后删除它。


(6)pop_front:头删

cpp 复制代码
void pop_front()
{
    erase(begin());              // 删除第一个有效节点
}

begin() 指向第一个有效节点,直接删除它。删除后原来的第二个节点成为新的第一个节点。

(7)clear:清空所有有效元素

cpp 复制代码
  void clear()
        {
            auto it = begin();
            while (it != end())
            {
                it = erase(it);          // 逐个删除,erase 返回下一个位置
            }
        }

clear() 用于清空链表中的所有有效元素,删除所有数据节点,让链表回到空链表状态(只剩哨兵节点)。


(4)类模板成员函数的实例化

1.类模板的实例化过程

当编译器遇到 list<int> lt; 这样的代码时,会先根据 int 这个具体类型,从类模板 list<T> 中生成一个具体的 list<int> 类,然后再用这个类创建出 lt 对象。

2.成员函数都是函数模板

类模板里的每个成员函数,本质上都是一个函数模板。在模板参数 T 确定之前,它们只是半成品,不是真正的函数。

3.按需实例化

类模板的成员函数不是一口气全部生成,而是按需实例化------程序里用到哪个成员函数,编译器才去生成哪个函数的代码。程序里没调用的成员函数,编译器就不会去生成它的机器码。

4.不调用就不检查

一个成员函数如果从来没被实例化过,编译器就不会去检查它内部的语法是否正确。即使里面有拼写错误、类型不匹配等问题,只要没被调用,程序

结合代码理解

cpp 复制代码
list<int> lt;           // 实例化 list<int> 类,成员函数还没生成
lt.push_back(1);        // 调用 push_back,此时才实例化 push_back
lt.push_back(2);        // 调用 push_back,已有实例化版本,直接使用
lt.push_front(3);       // 调用 push_front,此时才实例化 push_front
// pop_back 没有被调用,所以不会被实例化

cpp 复制代码
// 函数模板的按需实例化
// T* const ptr1
// const T* ptr2
template<class Container>
void print_container(const Container& con)
{
	// const iterator -> 迭代器本身不能修改
	// const_iterator -> 指向内容不能修改
	typename Container::const_iterator it = con.begin();
	//auto it = con.begin();
	while (it != con.end())
	{
		//*it += 10;

		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : con)
	{
		cout << e << " ";
	}
	cout << endl;
}

print_container 本身是一个函数模板,不是真正的函数。只有当你用具体的容器类型去调用它时,编译器才会根据那个类型生成对应的函数版本print_container 函数模板,当用 list<int> 去调用它的时候,编译器会把 Container 推导成 list<int>,然后生成一份专门处理 list<int> 的代码。如果用 vector<double> 去调用,编译器又会生成另一份专门处理 vector<double> 的代码。也就是说,同一个函数模板,传什么类型就按需生成什么版本的函数,不是提前把所有可能的版本都准备好。


四.list与vector的对比

vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:

五.总结代码

如下分为两个部分:

List.h

cpp 复制代码
#pragma once
#include<assert.h>

namespace bit
{
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		list_node(const T& data = T())
			:_data(data)
			, _next(nullptr)
			, _prev(nullptr)
		{
		}
	};

	template<class T, class Ref, class Ptr>
	struct list_iterator
	{
		typedef list_node<T> Node;
		typedef list_iterator<T, Ref, Ptr> Self;
		Node* _node;

		list_iterator(Node* node)
			:_node(node)
		{
		}

		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &_node->_data;
		}

		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		Self operator++(int)
		{
			Self tmp(*this);
			_node = _node->_next;

			return tmp;
		}

		Self& operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;

			return tmp;
		}

		bool operator!=(const Self& s) const
		{
			return _node != s._node;
		}

		bool operator==(const Self& s) const
		{
			return _node == s._node;
		}
	};


	/*template<class T>
	struct list_const_iterator
	{
		typedef list_node<T> Node;
		typedef list_const_iterator<T> Self;
		Node* _node;

		list_const_iterator(Node* node)
			:_node(node)
		{}

		const T& operator*()
		{
			return _node->_data;
		}

		const T* operator->()
		{
			return &_node->_data;
		}

		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		Self operator++(int)
		{
			Self tmp(*this);
			_node = _node->_next;

			return tmp;
		}

		Self& operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;

			return tmp;
		}

		bool operator!=(const Self& s) const
		{
			return _node != s._node;
		}

		bool operator==(const Self& s) const
		{
			return _node == s._node;
		}
	};*/

	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		/*typedef list_iterator<T> iterator;
		typedef list_const_iterator<T> const_iterator;*/

		typedef list_iterator<T, T&, T*> iterator;
		typedef list_iterator<T, const T&, const T*> const_iterator;

		iterator begin()
		{
			/*	iterator it(_head->_next);
				return it;*/
				//return iterator(_head->_next);
			return _head->_next;
		}

		iterator end()
		{
			return _head;
		}

		const_iterator begin() const
		{
			return _head->_next;
		}

		const_iterator end() const
		{
			return _head;
		}

		void empty_init()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

		list()
		{
			empty_init();
		}

		list(initializer_list<T> il)
		{
			empty_init();
			for (auto& e : il)
			{
				push_back(e);
			}
		}

		// lt2(lt1)
		list(const list<T>& lt)
		{
			empty_init();

			for (auto& e : lt)
			{
				push_back(e);
			}
		}

		// lt1 = lt3
		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			auto it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		void push_back(const T& x)
		{
			/*Node* newnode = new Node(x);
			Node* tail = _head->_prev;

			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;

			++_size;*/

			insert(end(), x);
		}

		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);

			// prev newnode cur
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
			prev->_next = newnode;

			++_size;

			return newnode;
		}

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* prev = pos._node->_prev;
			Node* next = pos._node->_next;

			prev->_next = next;
			next->_prev = prev;
			delete pos._node;

			--_size;

			return next;
		}

		size_t size() const
		{
			return _size;
		}

		bool empty() const
		{
			return _size == 0;
		}
	private:
		Node* _head;
		size_t _size;
	};

	struct AA
	{
		int _a1 = 1;
		int _a2 = 1;
	};

	// 按需实例化
	// T* const ptr1
	// const T* ptr2
	template<class Container>
	void print_container(const Container& con)
	{
		// const iterator -> 迭代器本身不能修改
		// const_iterator -> 指向内容不能修改
		typename Container::const_iterator it = con.begin();
		//auto it = con.begin();
		while (it != con.end())
		{
			//*it += 10;

			cout << *it << " ";
			++it;
		}
		cout << endl;

		for (auto e : con)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void test_list1()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		list<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
			*it += 10;

			cout << *it << " ";
			++it;
		}
		cout << endl;

		for (auto e : lt)
		{
			cout << e << " ";
		}
		cout << endl;
		print_container(lt);

		list<AA> lta;
		lta.push_back(AA());
		lta.push_back(AA());
		lta.push_back(AA());
		lta.push_back(AA());
		list<AA>::iterator ita = lta.begin();
		while (ita != lta.end())
		{
			//cout << (*ita)._a1 << ":" << (*ita)._a2 << endl;
			// 特殊处理,本来应该是两个->才合理,为了可读性,省略了一个->
			cout << ita->_a1 << ":" << ita->_a2 << endl;
			cout << ita.operator->()->_a1 << ":" << ita.operator->()->_a2 << endl;

			++ita;
		}
		cout << endl;
	}

	void test_list2()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		// insert以后迭代器不失效
		list<int>::iterator it = lt.begin();
		lt.insert(it, 10);
		*it += 100;

		print_container(lt);

		// erase以后迭代器失效
		// 删除所有的偶数
		it = lt.begin();
		while (it != lt.end())
		{
			if (*it % 2 == 0)
			{
				it = lt.erase(it);
			}
			else
			{
				++it;
			}
		}

		print_container(lt);
	}

	void test_list3()
	{
		list<int> lt1;
		lt1.push_back(1);
		lt1.push_back(2);
		lt1.push_back(3);
		lt1.push_back(4);

		list<int> lt2(lt1);

		print_container(lt1);
		print_container(lt2);

		list<int> lt3;
		lt3.push_back(10);
		lt3.push_back(20);
		lt3.push_back(30);
		lt3.push_back(40);

		lt1 = lt3;
		print_container(lt1);
		print_container(lt3);
	}

	void func(const list<int>& lt)
	{
		print_container(lt);
	}

	void test_list4()
	{
		// 直接构造
		list<int> lt0({ 1,2,3,4,5,6 });
		// 隐式类型转换
		list<int> lt1 = { 1,2,3,4,5,6,7,8 };
		const list<int>& lt3 = { 1,2,3,4,5,6,7,8 };

		func(lt0);
		func({ 1,2,3,4,5,6 });

		print_container(lt1);

		//auto il = { 10, 20, 30 };
	/*	initializer_list<int> il = { 10, 20, 30 };
		cout << typeid(il).name() << endl;
		cout << sizeof(il) << endl;*/
	}
}

Listtest.cpp

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

void test_list1()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);

	list<int>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	/*it = lt.begin();
	lt.erase(it + 3);*/

	// 不支持,要求随机迭代器
	//sort(lt.begin(), lt.end());

	string s("dadawdfadsa");
	cout << s << endl;
	sort(s.begin(), s.end());
	cout << s << endl;
}

struct A
{
public:
	A(int a1 = 1, int a2 = 1)
		:_a1(a1)
		, _a2(a2)
	{
		cout << "A(int a1 = 1, int a2 = 1)" << endl;
	}

	A(const A& aa)
		:_a1(aa._a1)
		, _a2(aa._a2)
	{
		cout << "A(const A& aa)" << endl;
	}

	int _a1;
	int _a2;
};

void test_list2()
{
	/*list<int> lt;
	lt.push_back(1);
	lt.emplace_back(2);
	lt.emplace_back(3);
	lt.emplace_back(4);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;*/

	list<A> lt;
	A aa1(1, 1);
	lt.push_back(aa1);
	lt.push_back(A(2, 2));
	//lt.push_back(3, 3);

	lt.emplace_back(aa1);
	lt.emplace_back(A(2, 2));
	cout << endl;
	// 支持直接传构造A对象的参数emplace_back
	lt.emplace_back(3, 3);
}

void test_list3()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
	lt.push_back(6);

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	auto it = lt.begin();
	int k = 3;
	while (k--)
	{
		++it;
	}

	lt.insert(it, 30);

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	int x = 0;
	cin >> x;
	it = find(lt.begin(), lt.end(), x);
	if (it != lt.end())
	{
		lt.erase(it);
	}

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

void test_list4()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(20);
	lt.push_back(3);
	lt.push_back(5);
	lt.push_back(4);
	lt.push_back(5);
	lt.push_back(6);

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	// 升序
	// lt.sort();
	// 降序 - 仿函数
	// less<int> ls;
	// greater<int> gt;
	// lt.sort(gt);
	lt.sort(greater<int>());

	// lt.reverse();
	//reverse(lt.begin(), lt.end());

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	std::list<double> first, second;

	first.push_back(3.1);
	first.push_back(2.2);
	first.push_back(2.9);

	second.push_back(3.7);
	second.push_back(7.1);
	second.push_back(1.4);

	first.sort();
	second.sort();

	first.merge(second);
}

void test_list5()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(20);
	lt.push_back(3);
	lt.push_back(5);
	lt.push_back(5);
	lt.push_back(4);
	lt.push_back(5);
	lt.push_back(6);

	lt.sort();
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	lt.unique();
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

void test_list6()
{
	// 一个链表节点转移给另一个链表
	std::list<int> mylist1, mylist2;
	std::list<int>::iterator it;

	// set some initial values:
	for (int i = 1; i <= 4; ++i)
		mylist1.push_back(i);      // mylist1: 1 2 3 4

	for (int i = 1; i <= 3; ++i)
		mylist2.push_back(i * 10);   // mylist2: 10 20 30

	it = mylist1.begin();
	++it;                         // points to 2

	mylist1.splice(it, mylist2); // mylist1: 1 10 20 30 2 3 4
	// mylist2 (empty)
	// "it" still points to 2 (the 5th element


// 调整当前链表节点的顺序
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
	lt.push_back(6);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	int x = 0;
	cin >> x;
	it = find(lt.begin(), lt.end(), x);
	if (it != lt.end())
	{
		//lt.splice(lt.begin(), lt, it);
		lt.splice(lt.begin(), lt, it, lt.end());
	}

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

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);
}

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<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);
}

#include"List.h"

// 10:35
int main()
{
	//test_list6();
	//test_op2();

	bit::test_list4();

	return 0;
}