1.list介绍
list在stl中是一种非常重要的容器,他的底层逻辑是我们之前的数据结构中的链表,而且这里是一种带头双向循环链表。
这是较为官方的文档介绍,大致意思就是:
- list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
- list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
- list与forward_list非常相似:最主要的不同在forward_list是单链表,只能朝前迭代,已让其更简单高
效。 - 与其他的序列式容器相比(array,vector,deque)list通常在任意位置进行插入、移除元素的执行效率
更好。 - 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素
list的图解:
就是这个图解的部分,所以list的实现可以说,我们大家都非常熟悉,但是list的难点在于他的迭代器实现,所以本章标题也围绕着list的迭代器。
2.list的使用
2.1list的构造
1:list(size_type n,const value_type&val=value_type())
主要用于构造list中包含的n个值为val的元素。
2:list()构造空list
3:list(const list&x)拷贝构造函数
4:list (InputIterator first, InputIterator last),用(first,last)区间中的元素构造list
2.2list的iterator的使用
还是那句话,list可以理解为一个指针,指向list中的某个节点。
注意:
1:begin于end为正向迭代器,对迭代器执行++操作,迭代器向后移动
2:rbegin(end)于rend(beegin)为反向迭代器,对迭代器执行++操作,迭代器向前移动
2.3list的capacity问题

2.4list的元素操作
1:push_front
2:pop_front
3:push_back;
4:pop_back
5:insert
6;erase
7:swap 用于交换两个list中的元素
8:clear 清空list中的元素
相信有了之前链表的学习,大家都知道,上面的接口都怎么使用,代表什么意思。
3.关于list的迭代器失效
list的迭代器失效情况,较为单一,最常见的就是erase后还访问pos处的位置,这就是完完全全的野指针访问了,一般情况下的erase接口的返回值一般是iterator,返回删除节点下一个节点的指针。
4.list的模拟实现
list的模拟实现,同我们之前的双向链表的搭建,一样,不同的是,这里的list是容器,而为了和算法进行配合,就多了一个迭代器,多这个迭代器不要紧,要紧的是这个迭代器全是坑,因为对比vector来说,list的存储形式并不连续,而且存储的是节点,我们不能对齐,直接使用,需要对list的迭代器单独实现。
4.1实现list的三个类
list是双向链表,所以实现链表就必须要有节点,然后之前我们提到的迭代器类,以及最后才是我们的list类。
总结:
节点,迭代器,list
4.2list节点类的封装
回顾我们之前自主实现链表的代码,节点的三个成员分别是next,prev,以及最后的data
有了这个类,我们做许多事情就方便的多,因为后续有list的增删改查,大部分都要创建新节点,有了这个类,我们只用:
Node* newnode=new Node;
这样我们就创建了与新的节点。
4.3list类的实现
因为我们之前说过,list是一种带头双向循环的链表,因此我们的list成员只用封装一个头节点_head即可:
下面是对头节点的初始化
接下来是实现list的size接口,其实就是寻找有几个节点,因此我们就可以利用迭代器的遍历完成。
还有一个非常重要的接口那就是插入接口,相信双向链表的增删改查大家都知道,我这里就不多解释,只提供代码了:
5.list迭代器类的封装
这里我们提到过list的迭代器是一个重点,它不同于之前的string以及vector,他并不是连续储存的,所以访问它不能直接进行++或--的操作,同时,我们对于迭代器的要求是我们可以用迭代器
iteartor it
来来实现对list中数据的访问,而不是节点的访问,所以节点的指针就不能用了,综合以上,我们只有对list的节点指针进行再封装,让其单独成为一个类,这时我们才能更好的实现后续的访问。
5.1list迭代器的成员
list迭代器的模拟实现是封装和重载
封装就是让其成为一个单独的类,重载就是当我们访问iterator时,看着是访问节点,实则是访问节点中的数据。所以,我们迭代器类的成员只有一个,那就是该节点:
5.2迭代器的重载
5.2.1解引用的重载
我们说了,解引用节点的地址,实则要解引用节点中存储数据的地址,因此,我们的解引用操作要实现,返回节点中的数据。
5.2.2箭头符号重载
箭头符号于解引用的操作相同,所以我们返回的不是节点的指针,而是节点中数据的指针:
5.2.3++的重载
我们知道迭代器有一个重要的玩法就是,当我们++时,迭代器就指向了下一个元素,而这里我们的迭代器要实现的就是返回下一个节点的地址,所以其逻辑和链表的遍历差不多:
5.2.4!=符号的重载
我们使用迭代器进行遍历时,一定有一种使用方法,那就是
while(it!=ls.end());
所以我们就要实现!=符号的重载。
同样也是比较两个节点的地址,不用比较数据
注意上面我的注释,带你重温以下类和对象的内容:
因为的it.end()的返回值时iterator的传值返回,所以会生成一个iterator的临时对象,而临时对象具有常性,因此我们这里的形参就必须是const的
5.3const迭代器实现优化
现在我们上面实现了一个普通迭代器的封装重载,但是还有一个重要的迭代器,就是const迭代器要怎么实现呢?或许你会这样想:
typedef const iterator const_iterator
然后重新写一个类,来实现const迭代器,(注意这里的const迭代器,指的是节点里的数据是const的,而不是节点是const的所以 list const iterator是错误的用法),但是你有没有想到过,我们这两种迭代器的不同,仅仅只是接口返回值的不同,其他的函数体都一样,那为什么我们不套用模板,来实现不同迭代器的复用呢?是不是一种特别漂亮的做法。
我们套用不同的迭代器种类,他就是不同的iterator,这样就通过模板实现了,迭代器的复用。
6.总结
我们list的类还有很多接口没有实现,我这里不写其一是有些接口确实不常用,其二就是我们已经谢过了string类和vector类,对数据结构肯定有了自己的认识,所以就不再赘述了,这篇文章主要还是为了,让大家搞明白,list的迭代器。