阿巴阿巴,list的博客来喽!
[TOC]
1.list是嘛玩意?
之前的vector是一个顺序表。总所周知,学完顺序表肯定不能不学链表,所以list就来了!
list是一个可以在任何地方进行插入删除的序列式容器,可以进行前后双向迭代
说人话就是:list是一个双向带头循环链表
这不巧了嘛!之前我写过用C语言实现双向带头循环链表的博客
其优缺点也很明显
- 支持快速插入删除
O(1)
- 支持前后双向迭代访问
- 不支持任意位置的随机访问
STL中的list也满足上面的这些优缺点
话不多说,来看看list的函数接口吧!
函数接口
大部分接口和前面所学的两个容器都是一样的啦,这里就不贴完整截图了(见上方链接)
这里还多了一个emplace_front
,看完函数解释后可知它也是一个头插
2.简单了解使用
2.1构造
list支持的构造函数如下
这些函数和vector
完全一致,这里就不过多介绍了
2.2迭代器
迭代器 | 说明 |
---|---|
begin+end | 获取第一个数据位置iterator/const_iterator, 获取最后一个数据的下一个位置iterator/const_iterator |
rbegin+rend | 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的 reverse_iterator |
都是老样子,没有啥好说的
除了迭代器以外,一般的容器都会提供front
和back
两个参数来访问头部/尾部数据。但是我们并不常用这个,迭代器用的更多一些
2.3长度操作
库函数中给我们提供了两个和list长度相关的操作,因为是链表,所以也不存在扩容问题,每一个节点都是一个独立的空间
- empty 判断是否为空,是空返回true
- size 计算list中有效数据长度
关于插入和删除操作,list和vector/string
基本都是一致的,学会一个基本都会用!
2.4迭代器失效问题
和vector一样,list也会遇到一定程度的迭代器失效问题。
因为list是一个链表,其在插入操作的时候是在迭代器所指节点前一个位置进行插入,不会影响该节点和这个节点之后的结构,也不会导致迭代器失效。
只有在删除的时候才会导致迭代器失效
解决办法也很简单,使用迭代器删除数据的时候,接收返回值更新一下迭代器即可!
基本了解了函数接口后,让我们来试试模拟实现吧!
3.模拟实现
模拟实现和STL-list源码见我的Gitee
下图是之前C语言版本链表博客里,我画的双向带头循环链表
的结构图
list的结构和这个图片是一样的。但因为我们现在所使用的是C++中的类和对象,所以我们的操作都需要尊崇stl库的命名规则和使用方法,其结构的实现也会有所不同。
比如在STL源码中可以看到,list的主结构中只有一个node
,即头节点。
3.1 节点结构
在list中,我们不直接在list
主类中放入单个节点的结构,而是使用一个自定义类型作为节点。
复习:在
struct
中,成员默认是共有
1 | template<class T> |
这样后面的插入删除操作就可以直接new一个新的node,并调用构造函数完成_data
的赋值。
在list
的主类中,我们遵循库的方法,只用一个头节点的指针作为成员
1 | private: |
3.2 插入删除
因为之前写过C语言的代码,关于插入和删除操作相对较为熟练,代码如下👇
1 | //在pos之前插入 |
而头插/头删/尾插/尾删这类方法,我们直接复用insert和earse即可!
3.3 迭代器(重要)
在上面的插入和删除代码中,我已经使用了迭代器作为参数和返回值。由于list的主类中并没有直接存放3.1
节点结构,我们需要自己单独完成一个迭代器的类,来实现迭代器的相关操作
- vector/string是顺序表,迭代器可以直接用指针替代,在本类中模拟实现
- list是顺序表,迭代器的
+和-
操作其实是通过next
和prev
指针往后往前找内容,所以需要单独的模拟实现
在STL源码中的list也是单独重载了一个迭代器类
其基本结构如下
1 | template<class T, class Ref, class Ptr> |
这里解释一下为何有3个模板参数:ref指代引用,ptr指代指针
因为在后续的操作中我们需要传入T的引用T&
和指针T*
,如果之传入一个T
,编译器无法确认是否为const类型,也就无法阻止用户使用迭代器修改值,成员的数据就不够安全,且const的成员函数无法调用迭代器。
重载3个模板参数后,我们就可以用传入的模板参数来控制const类型
1 | typedef __list_iterator<T, T&, T*> iterator; |
基本的结构弄清楚之后,下面一起来看看,迭代器的++和--
,解引用以及指针->
分别对应了链表的什么操作呢?
3.3.1 加减
好吧,前面已经提到过了,我们需要用next/prev
指针来完成加减操作
1 | //前置++ |
注意前置和后置的区别,后置加减需要先用一个tmp
变量储存原本的迭代器位置,再让迭代器指向下一个节点
而当我们解引用迭代器的时候,想获取的其实是_data
的值,而不是节点的地址(这里迭代器依旧是用指针模拟的)
3.3.2 解引用和指针操作
所以重载后的解引用操作如下
1 | Ref operator*() |
3.3.3 判断相等/不相等
最后判断相等和不相等就很简单了,因为本身就是指针,我们直接调用原生的!=
进行判断即可
1 | bool operator!=(const self& it) |
到这里,一个建议版本的正向迭代器我们就实现啦!
3.3.4 begin/end
这里我们需要在List的本类中获取迭代器的begin
和end
,这里我们是调用了迭代器的构造函数,构造出来一个迭代器并进行返回
1 | const_iterator begin() const |
现在就可以愉快的用迭代器进行打印操作了!
3.4 构造函数
有了迭代器,现在就可以完成库函数里面的几个构造函数了(主要是迭代器区间的那个函数)
默认构造函数如下,先创建一个头节点,再让它的前后都指向自己。在C语言的初始化函数中,我们也是这么做的
1 | list() |
但其他的构造函数由于_head
为空,所以我们需要单独实现一个空初始化函数来创建头节点
1 | //当使用其他构造函数的时候,head还不存在,需要手动构造一个 |
当然,上面那个空的构造函数list()
也可以复用empty_init()
,这样代码更整洁
迭代器区间构造和vector
是一样的,其主要是解引用后直接将值进行头插(这里就用上了之前迭代器里重载的解引用操作)
1 | template <class InputIterator> |
而拷贝构造就可以直接复用迭代器区间构造,赋值重载就直接swap即可。这部分的实现和vector
是一样的,有问题可以在评论区提出哦!
1 | void swap(list<T>& lt) |
3.5 析构函数
因为链表需要我们一个一个去删除每一个节点的空间,这里需要单独实现一个clear
函数供析构函数使用
1 | void clear() |
上面这个函数就有些麻烦,我们其实可以直接复用erase
进行删除操作+更新迭代器!
1 | void clear() |
到这里,一个基本的list就模拟实现完成了!
3.6 操作自定义类型
我们刚刚实现的list和STL
库中的一样,都可以存放自定义类型
1 | struct TA |
但在打印和访问自定义类型的时候,我们重载的解引用操作符就不能用了,这就是为啥要重载->
操作符,此时我们还可以通过->
获取到迭代器所指的对象的成员,这时候直接打印成员变量即可!
你可能觉得,这不对啊!之前重载的这个操作符不是返回的节点_data
的地址吗?为啥可以直接访问_data
的成员?
1 | Ptr operator->() |
实际上,这里应该是it->->_a1
,但是编译器在处理的时候直接将两个访问箭头简化成了一个,即增加了代码可读性,也方便使用了!
3.7 反向迭代器(适配问题)
关于反向迭代器,这里牵扯到一个更深的适配问题。我用我的笨话稍微解释一下,有问题或者有啥错误的话欢迎在评论区提出!😂
我们知道,反向迭代器是从后往前走的,它的加减操作和正向迭代器相反。
那么比起单独实现一个反向迭代器的类,我们可否利用正向迭代器,直接适配出一个反向迭代器呢?毕竟看起来它们只有方向不同!
1 | template<class Iterator, class Ref, class Ptr> |
说上就上,这里我们先将反向迭代器设定为和正向相似的结构,不过它的模板参数变成了正向迭代器,而不是参数类型T,这在后续typdef
的时候需要注意(不然会报错)
除了下面的解引用操作之外,其他只需要吧正向迭代器反过来用,就可以达到我们的目的!
1 | Ref operator*() |
解引用访问前一个数据的原因是,我们判断结束是用!=rend()
,而rend()
指向的是列表的第一个有效数据,如果直接解引用当前内容,最后一个数据就无法访问到,出现了缺漏
1 | //前置++ |
更加完整的源码可以去看我的Gitee仓库哦!
完成了上面的代码后,我们需要在list本类里面进行一波操作,让它能够支持反向迭代器
1 | //支持反向迭代器 |
注意上面的模板参数,第一个需要传入正向迭代器,而不是T
1 | //调用反向迭代器的构造函数 |
用一个打印代码便可以测试出我们的操作是否正确!
可以看到,完美逆向打印出了内容!
利用正向迭代器进行适配的最大好处,那就是其他模拟实现的容器也可以直接调用这个反向迭代器,只要在本类中加入上面的typedef
和4个函数即可!
就以上篇博客的vector
为例,加上上述代码后,她也能支持反向迭代器了!(源码也上传到仓库里面了)
结语
本次list的模拟实现,我们尝试模拟了一个更加详细的迭代器类,并实现了反向迭代器
有任何问题,或者本博客有错误,都欢迎在评论区提出哦!
- 本文标题:【C++】STL:list
- 创建时间:2022-07-16 16:10:46
- 本文链接:posts/1456870854/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!