我在C阶段写的链表中就定义了两个结构体,一个是结点的结构体,一个是链表的结构体。本篇我们用C++模拟实现带头双向循环链表,要定义三个自定义类型,一个结点的,一个链表的,一个迭代器的。其中迭代器时一个重难点,也可帮我们更好的学习和理解C++知识点。
我们用C++写的一个好处就是不需要再写一个全局的typedef来决定每个结点内部存放的元素的数据类型了,只需要用模板即可,而且还可定义存放不同的数据类型的对象。
然后把list的定义给出:
带头双向链表,需要的就是一个哨兵位的头结点就行,结构上是实现决定的。
链表的无参构造,需要先把头结点搞出来,然后再将头节点的next和prev指向其本身就行了。
在C阶段写申请一个节点的空间时,要单独写一个函数来实现,而这里就不需要了,因为new自定义类型的时候会自动调用其构造函数,所以我们只需要将Node的构造函数写好就行了。
上面的就是带缺省值的构造。如果没有传参x的值就是T()。
记得传引用,不然可能出现T为自定义类型的情况,此时若是传值就会有没必要的消耗。
然后我们上面List的构造中,new之后就会调用Node中的构造函数,我们还可以给值。但是这里构造函数构造的是哨兵位的头结点,没有必要给值,只是起一个占位的作用。
基本框架已经搭好了,然后我们就可以写一个push_back来实现基本使用。
很简单,和C语言时期的思想一样,大家画个图就行了,我这里在我自己的画图板上画,如果你也想动手模拟实现的话,我建议自己画一下图。
前一篇博客list介绍中说了,如果我们想要遍历list的话,得要用迭代器,但是list的迭代器与string和vector的迭代器可不同,后面两者的空间是连续的,这两个的迭代器都是原生指针就足够完成所有的迭代器所需的操作(例如++、*等等);而list空间是不连续的,所以我们想要用list迭代器的时候就要自己定义迭代器,因为如果list还用原生指针的话,一些操作符是用不了的,比如说++,空间不连续,就算++了也有可能跑到一个未申请的空间上去,就出问题了,还有*,迭代器解引用之后返回的值是什么,这都是需要自己定义的。
但C++的优势就在于类的封装和运算符的重载。
先给出遍历list的代码:
我们需要实现的函数有begin()、end()、*、++、!=。
就先按照这四个来给。
其中begin和end是在list中的。
*、++和!=是在迭代器中的。
我们先把迭代器简单定义一下:
对比一下string和vector就能知道list的迭代器表示的是什么了。
简单来说就是结点的地址。
代码如下:
然后再写一下list的beign()和end()。
简单解释一下,返回值为iterator,返回的是结点的指针,返回时会调用迭代器的构造函数,构造出一个迭代器。
begin的位置是头结点的下一个结点。
迭代器区间是左闭右开的也就是[first, last),最后一个有效节点的下一个位置就是_head,所以end的位置就是头结点的位置。
string和vector的非const对象的迭代器解引用后返回的都是一个T类型的引用。
所以list也就返回T的引用就行。
迭代器的位置往后挪一个,就是让迭代器中的结点指针跑到下一个位置。
写这个返回值要在__list_iterator中也把其typedef一下:
两个迭代器判断是否不相等,只要比较二者中的结点指针是否相同即可。
上面这个五个实现了就能遍历链表了。
此时迭代器基本实现了,就可以用范围for了。
我们再实现一些迭代器的函数:
这个加了int参数的是后置++。
要先记下来++前的迭代器,然后再让内部的指针后移,然后返回记录下的迭代器。
不要问为什么带了int参数的就是后置,这是龟腚。
箭头重载。
讲这个之前大家先想一想我们用什么类型会要用到->?
答案是自定义类型。
我们平时用内置类型的指针时是不会用到箭头的,只有我们用自定义类型的指针时才会用到内置类型。
我现在定义一个pos类代表坐标类。
然后定义对象如下:
内置类型不会用到 ->,自定义类型用到->。
当然自定义类型也可用*,但是很麻烦:
我们定义list对象时,类型T如果为int这样的内置类型,解引用之后会返回int的值。
那么当我们定义list对象时,类型T为自定义类型呢,会返回T类型的对象,比如所前面的pos,如果传的是pos,那么就会返回pos类型的对象,如果我们呢想要再访问pos内部的成员呢?
把operator和operator->放一块看看:
直接讲了,->返回值是&(operator()),得到的是&(_node->_data),也就是T类型对象的地址。
此时我们能直接用。
it->这个返回值为pos的地址,以我们之前所学的知识,如果我们想要再访问pos地址中的成员时,还要再加一个->才行,所以本来应该是it->->_row。但是这里是it->_row,直接一个箭头就访问到了pos的成员。
这是为了语法的可读性,编译器做了特殊处理,自动帮我们省略掉了一个->。
以后的map、智能指针等也会有这样的operator->的重载,用的时候都是自动省略一个箭头,只要记住,->返回的是数据的地址,也就是原生指针,原生指针加->就访问到了成员。
前面说的都是非const的对象,那么const的对象迭代器怎么搞。
非const对象是可以调用const成员的,但是非const对象就不能调用非const的成员了。
现在定义如下函数,功能是遍历链表:
参数给的是const &的,但是这是出问题了,我们前面没有实现const对象的beign和end。
给出如下错误的beign和end:
此时虽然能遍历了,但是是有问题的,我们可以通过其中的迭代器修改const链表。
此时不会报错,但是其实不是我们想要的。
那么我们就还要实现以下const的迭代器。
怎么实现呢?
有的同学说可以将非const的代码拷贝一份再改为const的代码。
可以是可以,就是比较麻烦。
我们看一下STL库中是怎么实现的:
链表中:
可以看到,库中list类中重命名迭代器的时候多了两个模板参数。分别是引用和指针。
有什么用呢?
再看__list_iterator的模板参数有三个,T、REF、PTR。
答案揭晓,当我迭代器传参为非const的T、T&、T时就会实例化出非const的迭代器,当我迭代器传参为const的T、T&、T时就会实例化出const的迭代器。
那么就好说了,我们也把这些模板参数改改。
__list_iteartor中改一下模板参数,并把所有的T*改为Ptr,把所有的T&改为Ref:
list中改一下iterator的typedef并加上const_iterator的begin和end:
然后const的就能跑起来了:
这里是一个难点,不懂的同学好好钻研钻研。主要是const和非const迭代器中每个函数的返回值要搞清楚是为什么那样写的。
插入,在pos的前一个位置插
测试:
但此时若用算法库中的find会报错:
因为我们iterator的实现不符合STL的规范,需要typedef一堆东西,加到__list_iterator中。
STL龟腚一个规范的迭代器要加上一些东西:
加上了之后就能用库中的find了。
看:
我们可以用insert来复用一下push_back和push_front。
再写一下erase。
测试一下:
–end是因为,end()是哨兵位头结点,不能删除,所以要–一下,变为最后一个有效结点。
再借用一下find:
记得list的erase会出现迭代器失效的问题:
上面的代码崩掉了。
不要多次重复删除同一位置的迭代器不修改。不然就是野指针。
库中的和我这里的实现都是要将删除的后一位返回。用上这一点就不会崩掉。
然后还可以用erase来复用pop_back和pop_front。
测试:
就剩下一些尾没收了,马上结束,看到这的再坚持一下。
前面我们是没有写拷贝构造的,析构也没写。
上面没有拷贝构造,就会出现浅拷贝的问题,但是代码运行起来没有崩掉,因为我们还没有写析构,默认的析构是不会释放我们开的空间的。
如果我们此时手动释放空间就会崩掉。
但是我们写析构之前要写一下clear函数,这个是stl库中有的,功能就是将链表中的结点清空,只留下哨兵位头结点。
然后再复用一下clear来写析构。
这里归根结底的问题还是默认的拷贝构造导致浅拷贝了,所以我们要重新写一下拷贝构造。
只要这个类有动态开辟空间的成员就都是要写这个的。不然就有浅拷贝的问题。
传统写法,我们可以挨个遍历链表,一个个的push_back。
但是比较麻烦,我们可以复用一个构造函数,就是迭代器区间构造。
用这个迭代器区间构造来造出来一个链表,然后再将该链表与原链表交换。
库中是有这个迭代器区间构造的函数的。
所以先实现一下迭代器区间构造:
先把要构造的链表初始化,然后再将构造的数据一个个push_back就行。
如果不初始化就会崩掉,因为插入第一个时要用到头结点,但是没初始化,next和prev都是指向未知的。
先写一下初始化的函数,其实就和无参构造一样。
写了这个之后无参构造里面直接调用init就可以了。
然后我们复用一下迭代器区间构造来实现拷贝构造。
先初始化(不初始化也会崩),再构造出来一个对象,然后进行交换。
不初始化会崩掉是因为最后交换的时候,将未初始化的数据交还给了tmp,然后tmp生命周期只在list这个构造函数中,调用结束,tmp调用其析构,delete了未开辟的空间。
下面这个swap中的::swap是库中的swap。
交换一下二者的哨兵位头结点就好了。
然后就是赋值运算符重载了,也是默认的不行,会浅拷贝,所以要自己写一下。
上面的是简便的写法,ls为拷贝,拷贝完了再将原来的数据与其交换。
最后说一个东西。
例如:
一般情况下都是要加
再比如说迭代器中的:
ListNode是类外的,不能省略。
但__list_iterator是类内的,所以可以省略。
最后这一点不做强求,只是让大家了解一下,以后遇到了不懵B就行。
到此结束。。。