文章目录
- 1. list的介绍
- 2. 迭代器的分类
- 3. list的构造
- 4. list的实现
- 4.1 list的基本结构
- 4.2 list的push_back函数
- 4.2 list的迭代器
- 4.2.1 operator- >
- 4.2.2 const迭代器
- 4.3 insert函数
- 4.4 earse函数
- 4.5 迭代器失效问题
- 4.6 析构函数
- 4.7 构造函数
- 4.8 拷贝构造
- 1. 传统写法
- 2. 现代写法
- 4.9 赋值运算符重载
1. list的介绍
1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器(时间复杂度O(1)),并且该容器可以前后双向迭代(++/- -)。
2. list的底层是带头的双向循环链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代。
2. 迭代器的分类
从实现结构的角度,迭代器在实际中分为3类:
第一种:单向迭代器。只支持++
比如:forward_list,unordered_map,unordered_set
第二种:双向迭代器。支持++和- -
比如:list,map,set
第三种:随机迭代器。支持++,- -,+,-
比如:vector,string,deque
不过在单向迭代器中可以传双向迭代器和随机迭代器,双向迭代器可以传随机迭代器。
3. list的构造
这些和我们之前所说的string和vector是差不多的。举个例子演示一下就可以了。
4. list的实现
4.1 list的基本结构
首先,我们看一下源代码的实现结构:
成员变量:
成员变量它只有一个link_type类型的节点。
link_type类型是一个__list_node< T >的模板指针。
__list_node的结构如下:
这里的指针类型是void*,这里也可以写成__list_node< T > *
那么我们是如何知道它是带头循环双向链表?
我们先看一下它的构造函数:
它在无参构造里调用了empty_initialize()函数:
这个初始化就是获得一个节点,节点的next指向自己,节点的prev也指向自己。所以它初始化是一个头节点。
我们再看一下它的插入函数:
从这两行可以看出它是一个双向循环链表。
下面我们就把这个基本结构写一下:
4.2 list的push_back函数
带头的双向循环链表我们以前的文章也说过,很简单,直接上代码:
4.2 list的迭代器
既然是带头的双向循环指针,那么它的结构是这样的:
我们知道迭代的begin()指向第一个数据,end()指向数据最后一个元素的下一个位置(也就是哨兵位头节点)。
那么我们看一下标准库里是如何使用的:
可以看到这个迭代器可以++,解引用时就可以打印数据。那么我们想一下,如果list的迭代器是指针,它是指向这个节点的指针,是如何解引用就拿到数据的呢?所以list的迭代器不是一个指针了。而是封装成了一个类。
下面我们就需要把迭代器的解引用,++和不等于,这三个运算符重载写出来。
解引用很简单,直接返回节点的数据就行了。
不等于就是看节点地址是否相同。
前置++,返回++后的迭代器。
然后,我们要在list类里定义迭代器的begin和end:
这里我们把迭代器模板重命名,这样用户用迭代器时就不需要关心底层了。
这里的begin()和end()函数里面写的是匿名对象,然后传值拷贝。
我们也可以这样写:
是一样的。我们也可以这样写:
这是单参数的隐式类型转换,可以默认构造一个迭代器。
那么我们这里的迭代器,需不需要写析构呢?
答案:不需要。因为节点不属于迭代器,不需要迭代器释放。不过编译器会默认生成一个,但什么事情都不干。
那么这里的迭代器,需不需要写拷贝构造和赋值重载呢?
答案:不需要。默认生成的就行了,浅拷贝就可以。因为要指向同一块空间。
那么我们现在就来验证一下,看一看写的对不对:
是可以使用的。
然后,我们再来完善一下它的后置++和前置,后置- -,和==
4.2.1 operator- >
我们要知道,迭代器的作用是模仿指针的功能,我们先看下面的代码:
指针可以使用箭头找到自定义类型的成员,那么迭代器这里也应该可以。
我们就需要重载。
有同学对这样的代码感觉很疑惑。返回的是一个数据的地址。
如下图所示:
但是AA也是一个自定义类型,所以我们还应该加一个箭头,这样写:
但是不对,这样写就对了:
这是因为编译器为了可读性进行优化处理,优化以后,省略了一个。
4.2.2 const迭代器
在这里,我们要调用一个打印函数,但是它是被const修饰的。所以我们调用普通的迭代器就不行了。那么我们改成这样呢?
虽然可以运行了,但是返回的迭代器是可以修改的。但我们是一个const的迭代器,只能读,不能写。这样就违反意愿了。
那么我们再修改一下:
这样虽然不能修改了,但是普通的迭代器也不能被修改了。
那么我们此时该怎么办呢?大佬们是这样做的:
我们给这个迭代器的模板添加了两个参数。然后,我们就可以用一个模板实例化两个不同的迭代器类型。
然后我们将这个类型typedef一下,因为类型有点长了,下面改起来麻烦。
在list模板类里面,我们把普通迭代器和const迭代器都重定义一下:
这样调用的时候会选择最匹配的一个:
我们看一下匹配的过程,如下图所示:
普通迭代器的Ref是T&,Ptr是T*
const迭代器的Ref是const T&,Ptr是const T*
为什么这里第一个模板参数T,不加const呢?
如果我们这里加上const,那么我们看一下这里:
我们传递的是_head- >_next,它的类型是:list_node< int > *,但是我们想构造一个const迭代器,我们看这里:
这里的node类型是list_node< const int > *,所以和我们传的就不匹配了,就会出现错误。
4.3 insert函数
这样push_back和push_front就可以复用了。
尾插就在end()处插入就行了,头插就在begin()处插入。
4.4 earse函数
头删也很简单就是begin(),尾删只需要将end迭代器减减一下:
4.5 迭代器失效问题
我们先看下面的代码:
这个代码是将链表中的偶数删除,如果不是的就it++,我们看一下运行结果:
我们看到并没有进行正确的删除,原因如下:
如果此时我们将2删除了,那么it就变成了一个野指针。在源码中它是这样做的:
它是加了一个迭代器的返回值。这个返回值的意思是:指向函数调用擦除的最后一个元素后面的元素的迭代器。如果擦除了序列中的最后一个元素,则容器结束。
那么有了返回值,我们就可以这样写:
验证一下:
那么insert呢?因为insert插入,它不会扩容,插入进去迭代器还是指向原来的节点,没有发生改变。所以insert不会发生迭代器失效的问题。但是在源代码里,还是给它加了返回值。
它的返回值是:指向第一个新插入元素的迭代器。
4.6 析构函数
我们先写一个clear函数:
这个函数的意思就是:从列表容器中删除所有元素(这些元素将被销毁),并使容器的大小为0。
实现如下:
现在我们再来写析构函数:
4.7 构造函数
在list里还有一个用迭代器来构造的函数:
这个很容易理解,就是你输入的迭代器区间来构造这个list对象。
在构造之前,我们肯定要把哨兵位的头节点先弄出来。在源代码里是封装了一个函数来弄的:
现在,我们再构造就很简单了:
4.8 拷贝构造
在这里,如果我们不写,编译器会默认构造一个拷贝构造,但是是浅拷贝:
两个头指针(_head)指向同一块空间。如果析构的话就会析构两次。
所以名为要写深拷贝。
1. 传统写法
这个意思就是自己从lt里面取数据,然后去插入构造。
2. 现代写法
现代写法就是先构造一个tmp,然后交换两者的头指针。
4.9 赋值运算符重载
赋值运算符也是一样的道理,在传参数时就拷贝构造好,然后直接交换头指针就行。