一.结构体的构建

这个用结构体更好,因为我们需要不断的访问节点,类中的成员函数一般都是私有的,需要还用友元函数什么的。

这个是我们来实现的类,我们实现的是双向带头循环链表,这个是实用性最高的一个链表的形式。
这个_head就是一个头的作用,没有实际意义。
二.构造函数

这个就是我们的构造函数了,这个就是给头申请一块空间,让next和prev都指向头节点,这样就不会出现空指针的问题了,更加的方便。
三.push_back

这个是我们的插入函数,大家可以结合下面的图理解着看一下。

大家可以结合着上面的代码自己画一下图就很好理解了,这个就是创建一个新节点,然后先找到原来链表的最后一个节点也就是_head->prev,然后再让链表的最后一个元素指向新的节点,再让新的节点的next指向_head,再让新的节点的prev指向指向原链表的最后一个节点,最后让_head的prev指向新节点即可。
四.迭代器
下面来实现一下迭代器。
我们会遇到一些问题,我们难道也要像实现vector的迭代器一样,实现一个指针吗?
typedef list_node<T>* iterator;//这样实现会面临很多问题,我们的解引用是一个结构体而不是一个值,况且空间不连续,我们的++操作也没法完成,那我们该怎么办呢?
我们可以封装一个结构体来完成。

这就是我们迭代器的所需的一个结构体,就是通过node来接收我们的list_node<T>的结构体,通过构造初始化,就是传入我们需要的节点,接下来我们来完成迭代器的一些基本操作吧。
4.1 operator*()

通过赋值运算符的重载直接返回这个节点的值就可以了。
4.2 operator++()

这个就是我们的++操作,直接让我们的node=node->_next即可完成,也是很简单的。
4.3 operator!=()

这个就是两个list_iterator<T>对象进行比较的,通过比较它们的node是否相等即可。
4.4 operator==()

这个就是把上面的代码的!=改成等于了。
4.5 begin()和end()

这就是我们完成的begin和end函数,返回了iterator对象。
接下来我们来测试一下。



我们发现都是符合我们的预期的。
五.const的迭代器
我们都知道迭代器一般都有两种一种是普通版本的,一种则是const版本的。
const版本的实现起来也是很简单的。

这就是我们的const版本的迭代器,也是很简单的,只需要改一下返回值什么的就行了。

这是它的begin和end方法。

为什么报错了呢?
因为l1不是const类型的,它会优先匹配自己的迭代器,而不是const类型的迭代器,两个不同的类型都是优先匹配到自己的类型的begin和end方法。
这个就是it是const_iterator类型的,但是l1.end()是iterator类型的,类型不匹配。
我们发现出现了问题,无法使用*,因为我们的A类中没有重载。

如图所示就得到了解决,但是又发现了几个常犯的错误,那为什么我这里的l1调用不到const类型的begin和end函数呢?实际上,非 const 对象 l1 是可以调用 const 版本的 begin 和 end 函数的。不过,当非 const 对象调用成员函数时,编译器会优先选择非 const 版本的函数。
为什么我们的l2调用不到其他的函数呢?当你在qrh::list<A<int>>前面加上const后,该对象就成为了常量对象。对于常量对象,只能调用其const成员函数。
最后一条语句也可以验证我们上面所说的类型。
我们来看一下结果。

和我们想的一样,就是list_iterator类型的。
还有一种方法就是->的运算符,也是可以访问到自定义类型中的变量的。
5.1 operator->()

就是这种形式,可以大家刚开始看的时候有些迷茫,为什么返回node->_data的地址呢?
你可以理解为因为要返回一个指针,指针才具有->操作符。

我们来看一下,返回了node->_data的地址,应该是两个箭头的,第一个箭头先找到了_node->_data再来一个箭头才能访问得到其中的变量啊,因为编译器做优化了,两个箭头的可读性太低了,所以优化为一个箭头了,了解知道就行。
六.合并两个迭代器
上面是我们正常思路是要写两个的,但是我们可以观察一下它们的共同之处。

你看它们的这些方法只是返回值的类型不同罢了,一个是T,一个是const T,其他方法都是相同的,只有这两个方法需要区别一下,其实我们就可以使用函数模板,把它弄成一个就行了,接下来看我操作。

我们又加了两个模板参数,我们通过传参的不同可以区别这两个迭代器的类型。

看见没,我们通过控制后面两个参数的不同就可以区别出迭代器了,这是一个非常厉害的方法。
这样就可以把两个迭代器只写一个结构体就可以了。
我们接下来来完成剩下的一些常用的方法吧。
七.insert()

这个就是通过迭代器插入。
我们的思路是我们首先先找到我们所要插入的位置,然后再找到它的前一个位置,然后让前一个位置的_next指向newnode,newnode的_prev指向newnode,再让newnode的_next指向cur,再让cur的_prev指向newnode即可。
size表示链表的长度。
通过这个我们也可以简化一下我们的push_back了。

八.erase

这个就是通过迭代器位置删除该位置。
我们还是先找到我们需要删除的位置,然后再找到它的前一个位置和后一个位置,然后让前一个位置的_next指向下一个位置,让下一个位置的_prev指向前一个位置,再把当前位置的空间释放了,再把size--即可,最后的返回值的作用就是防止迭代器失效的,一会儿下面有样例。
九.push_front 和pop_back 和 pop_feont

我们等会儿再写--的操作,这些都很简单,看一下就行了。

我们来测试一下我们写的代码。

发现是符合我们预期的。
十.operator--

这个就很简单了。
十一.后置--和后置++


这个我们在之前的运算符重载都讲过,这里就不在详细展开讲了,不懂的可以看之前的博客。
十二.clear

这就是我们的clear函数了,这里也体现了我们上面说的迭代器失效的问题了,如果我们不给返回值的话,此时it这个迭代器就会失效了,由于指向了一块被释放的空间,导致迭代器失效了。
十三.拷贝构造


不写的话就默认是浅拷贝了,可能会出现同一块空间被释放两次的场景。
举个浅拷贝的例子吧。

我们先跑一下看看。

我们发现程序崩溃了,就是发生了浅拷贝,两个对象指向了同一块空间,当其中一个对象被clear了,导致另一个对象指向了一块野指针的空间,导致打印不出来结果,此时程序就会崩溃了。
我们在写了深拷贝之后就不会出现这样的问题了。

没有问题。
十四.operator=()

这个是最新版的函数,想要详细的讲解可以看vector底层的那篇博客,里面有详细的讲解。
如果我们没有重载这个的话,默认还是浅拷贝,我们再来演示一下。

我们来看一下。

虽然打印出来结果了,但是程序还是崩溃了。原因就是浅拷贝使它俩指向了同一块空间,所以同一块空间被释放了两次,导致了程序的崩溃。
所以我们就必须写。
十五.结束语
感谢大家的查看,希望可以帮助到大家,做的不是太好还请见谅,其中有什么不懂的可以留言询问,我都会一一回答。 感谢大家的一键三连。
