1,双向循环链表基本结构的实现(不包含需要迭代器的部分)
先用struct封装链表的节点,这里我们仅需要提供一个构造函数即可,并且构造函数必须提供缺省值,因为会有如下使用场景:new Node();此时需要默认构造函数,所以必须有缺省值。
在list类模版中,先typedef 一下,这样方便使用Node<T>,
成员变量是双向循环链表的哨兵位头节点。
接下来是一些基本的接口函数:
空构造:仅仅需要new一个哨兵位头节点出来,但是要记得保证双向循环结构
push_back:传入一个T,不过T也有可能是自定义类型,所以要用&,const可以接收const对象和非const对象。
迭代器区间构造:
构造函数:构造时填入n个x。
值得一提的是,这个构造函数有可能跟上面的迭代器区间构造发生冲突,比如说:
list<int> lt2(5,1); 中5和1一般会被编译器默认为是int类型,于是:
对于迭代器区间构造函数:InputIterator会被推演为int,这样的话两个形参的类型都是int。
对于本构造函数:模版参数推演之后,两个参数分别是size_t和int。
系统认为两个int形参类型更加匹配,所以会去调用迭代器区间构造。
而迭代器区间的这个操作:
*first相当于对int进行解引用,所以会报错:非法的间接寻址
箭头所指处也更能证明我们的想法。
而解决方法只有重载一个如下图的函数:
这样就会调用这个函数,而不是迭代器区间的构造函数。
2,迭代器的实现以及相关函数的补充
我们知道list的底层空间不是连续的,所以原生指针的++和--不能达到我们预想的结果,我们所需要的是:++的时候走到它的next,--的时候走到它的prev。
将迭代器实现成一个类模版,对++ --以及一些其他的运算符进行重载可以解决这个问题。
2,1迭代器的实现
基本结构:
typedef是为了让代码更简洁易懂
成员变量是一个节点的指针
至于除了T之外的两个模版参数Ref和Ptr ,接下来会进行解释捏
构造函数:
++重载:
--重载:
判断是否为同一个节点:
重载*(解引用):
这里的返回值为什么不直接写成T&呢?
因为除了普通的迭代器,我们还要使用const迭代器,那样的话,返回值就是const T& 了,而const迭代器跟普通迭代器的结构极其相似,只有返回值不同,所以为了防止代码冗余,尽量复用代码,这才引入了模版参数Ref来手动控制。
重载->:Ptr模版参数的引入同上,是为了手动控制T和T*
你没看错,->的重载就是这么奇怪,这个需要我们以特殊的眼光来看待:
迭代器定义operator->的原因是:
迭代器是像指针一样的东西,如果是原生指针,解引用就可以获得对象,如果是一个结构,想要得到结构里的成员变量,就需要通过->来获得,比如日期类:
所有类型重载->都要这样,算是个奇怪的模版吧,至少我觉得很奇怪。
2,2相关函数的补充
看,这里我们手动传模版参数就可以实现iterator和const_iterator
begin,end,cbegin,cend:
erase:
clear:复用erase
析构函数:复用clear();
拷贝构造和赋值运算符重载:
这里很简单,唯独有一点要说的是,
拷贝构造函数是借助构造函数和std::swap来实现的,swap后,tmp对象会调用析构函数自动析构,
考虑到这一点,我们必须对this指向的对象进行处理,要不然就会把一个野指针交换给tmp,析构的时候会出现内存问题的。
在此之前,我们的处理都是将类中的指针类型的成员变量置为nullptr,
比如string:
比如vector:
但是这次不一样了,因为析构函数的内部必然需要对链表进行遍历,一边遍历一边释放节点,而遍历就需要用到iterator,iterator一旦++就会对当前节点进行解引用找到下一个节点,如果将_head置空,就会对空指针进行解引用。
所以我们要搞一个最简单的链表结构——空链表,以便于析构。
insert:
返回新插入节点的迭代器是为了符合官方库,不必在意这里。
3,反向迭代器的实现以及相关函数的补充
3,1反向迭代器的实现
Iterator这个模版参数,传的是谁的迭代器,实现的就是谁的反向迭代器,至于Ref和Ptr,
跟上面解释的一样。
成员变量就是某个容器的正向迭代器
构造函数:
重载*:
没错,解引用反向迭代器返回的是前一个位置的数据,这是因为下图:
由于rbegin和rend的位置跟begin和end的位置对称,所以,如果解引用返回当前节点的数据的话,就会访问到_head中的随机值,必须返回前一个位置的数据才行。
重载->:
重载++--
判断是否为同一个节点:
3,2相关函数的补充
这里是要先typedef出const_reverse_iterator的,因为如果先typedef出reverse_iterator的话,下面就会出现歧义。
ps:拷贝构造函数往往要对成员变量进行初始化,以便于交换之后析构。