【C++】手把手教你模拟实现list的基本功能

list模拟实现

    • 前言
    • 正式开始
      • list::构造
      • ListNode::构造
      • list::push_back
      • 迭代器(重点)
      • list::beign()
      • list::end()
      • iterator::operator*()
      • iterator::operator++()
      • iterator::operator!=(const iterator& x)
      • iterator::operator++(int)
      • iterator::operator==(const iterator& x)
      • iterator::operator--()
      • iterator::operator--(int)
      • iterator::operator->()
      • const迭代器
      • list::insert
        • insert复用的push_back
        • insert复用的push_front
      • erase
        • erase复用pop_back
        • erase复用pop_front
      • 析构
        • list::clear
      • 拷贝构造
        • 迭代器区间构造
      • 类内用类模板名可不加模板参数

【C++】手把手教你模拟实现list的基本功能_第1张图片

前言

我在C阶段写的链表中就定义了两个结构体,一个是结点的结构体,一个是链表的结构体。本篇我们用C++模拟实现带头双向循环链表,要定义三个自定义类型,一个结点的,一个链表的,一个迭代器的。其中迭代器时一个重难点,也可帮我们更好的学习和理解C++知识点。

正式开始

先把结点的基本定义给出:
【C++】手把手教你模拟实现list的基本功能_第2张图片

我们用C++写的一个好处就是不需要再写一个全局的typedef来决定每个结点内部存放的元素的数据类型了,只需要用模板即可,而且还可定义存放不同的数据类型的对象。

然后把list的定义给出:
【C++】手把手教你模拟实现list的基本功能_第3张图片
带头双向链表,需要的就是一个哨兵位的头结点就行,结构上是实现决定的。

list::构造

链表的无参构造,需要先把头结点搞出来,然后再将头节点的next和prev指向其本身就行了。
【C++】手把手教你模拟实现list的基本功能_第4张图片
在C阶段写申请一个节点的空间时,要单独写一个函数来实现,而这里就不需要了,因为new自定义类型的时候会自动调用其构造函数,所以我们只需要将Node的构造函数写好就行了。

ListNode::构造

【C++】手把手教你模拟实现list的基本功能_第5张图片

上面的就是带缺省值的构造。如果没有传参x的值就是T()。

记得传引用,不然可能出现T为自定义类型的情况,此时若是传值就会有没必要的消耗。

然后我们上面List的构造中,new之后就会调用Node中的构造函数,我们还可以给值。但是这里构造函数构造的是哨兵位的头结点,没有必要给值,只是起一个占位的作用。

基本框架已经搭好了,然后我们就可以写一个push_back来实现基本使用。

list::push_back

很简单,和C语言时期的思想一样,大家画个图就行了,我这里在我自己的画图板上画,如果你也想动手模拟实现的话,我建议自己画一下图。

代码:
【C++】手把手教你模拟实现list的基本功能_第6张图片
对于是否有结点都是适用的。
【C++】手把手教你模拟实现list的基本功能_第7张图片
测试是成功的。

前一篇博客list介绍中说了,如果我们想要遍历list的话,得要用迭代器,但是list的迭代器与string和vector的迭代器可不同,后面两者的空间是连续的,这两个的迭代器都是原生指针就足够完成所有的迭代器所需的操作(例如++、*等等);而list空间是不连续的,所以我们想要用list迭代器的时候就要自己定义迭代器,因为如果list还用原生指针的话,一些操作符是用不了的,比如说++,空间不连续,就算++了也有可能跑到一个未申请的空间上去,就出问题了,还有*,迭代器解引用之后返回的值是什么,这都是需要自己定义的。
但C++的优势就在于类的封装和运算符的重载。

先给出遍历list的代码:

【C++】手把手教你模拟实现list的基本功能_第8张图片

我们需要实现的函数有begin()、end()、*、++、!=。
就先按照这四个来给。
其中begin和end是在list中的。
*、++和!=是在迭代器中的。

我们先把迭代器简单定义一下:

迭代器(重点)

对比一下string和vector就能知道list的迭代器表示的是什么了。
简单来说就是结点的地址。

代码如下:

【C++】手把手教你模拟实现list的基本功能_第9张图片

然后再写一下list的beign()和end()。

list::beign()

简单解释一下,返回值为iterator,返回的是结点的指针,返回时会调用迭代器的构造函数,构造出一个迭代器。
begin的位置是头结点的下一个结点。
【C++】手把手教你模拟实现list的基本功能_第10张图片

list::end()

迭代器区间是左闭右开的也就是[first, last),最后一个有效节点的下一个位置就是_head,所以end的位置就是头结点的位置。
【C++】手把手教你模拟实现list的基本功能_第11张图片

iterator::operator*()

string和vector的非const对象的迭代器解引用后返回的都是一个T类型的引用。
所以list也就返回T的引用就行。
【C++】手把手教你模拟实现list的基本功能_第12张图片

iterator::operator++()

迭代器的位置往后挪一个,就是让迭代器中的结点指针跑到下一个位置。
写这个返回值要在__list_iterator中也把其typedef一下:
【C++】手把手教你模拟实现list的基本功能_第13张图片

然后就能写了:
【C++】手把手教你模拟实现list的基本功能_第14张图片

iterator::operator!=(const iterator& x)

两个迭代器判断是否不相等,只要比较二者中的结点指针是否相同即可。
【C++】手把手教你模拟实现list的基本功能_第15张图片

上面这个五个实现了就能遍历链表了。
【C++】手把手教你模拟实现list的基本功能_第16张图片
此时迭代器基本实现了,就可以用范围for了。
【C++】手把手教你模拟实现list的基本功能_第17张图片
我们再实现一些迭代器的函数:

iterator::operator++(int)

这个加了int参数的是后置++。
要先记下来++前的迭代器,然后再让内部的指针后移,然后返回记录下的迭代器。
在这里插入图片描述
不要问为什么带了int参数的就是后置,这是龟腚。

iterator::operator==(const iterator& x)

不等的重载给了,==的也给出来。
【C++】手把手教你模拟实现list的基本功能_第18张图片

iterator::operator–()

前置–
【C++】手把手教你模拟实现list的基本功能_第19张图片

iterator::operator–(int)

后置–
【C++】手把手教你模拟实现list的基本功能_第20张图片

iterator::operator->()

箭头重载。
讲这个之前大家先想一想我们用什么类型会要用到->?

答案是自定义类型
我们平时用内置类型的指针时是不会用到箭头的,只有我们用自定义类型的指针时才会用到内置类型。

我现在定义一个pos类代表坐标类。
【C++】手把手教你模拟实现list的基本功能_第21张图片
然后定义对象如下:
在这里插入图片描述
内置类型不会用到 ->,自定义类型用到->。
当然自定义类型也可用*,但是很麻烦:
【C++】手把手教你模拟实现list的基本功能_第22张图片

我们定义list对象时,类型T如果为int这样的内置类型,解引用之后会返回int的值。

那么当我们定义list对象时,类型T为自定义类型呢,会返回T类型的对象,比如所前面的pos,如果传的是pos,那么就会返回pos类型的对象,如果我们呢想要再访问pos内部的成员呢?

可以用*解引用:
【C++】手把手教你模拟实现list的基本功能_第23张图片
但是如果我们想用箭头呢?

此时是不能直接用的:
【C++】手把手教你模拟实现list的基本功能_第24张图片
会报错,我们得自己重载出来。

代码如下:
【C++】手把手教你模拟实现list的基本功能_第25张图片
比较晦涩。

把operator和operator->放一块看看:
【C++】手把手教你模拟实现list的基本功能_第26张图片
直接讲了,->返回值是&(operator
()),得到的是&(_node->_data),也就是T类型对象的地址。

【C++】手把手教你模拟实现list的基本功能_第27张图片

此时我们能直接用。

it->这个返回值为pos的地址,以我们之前所学的知识,如果我们想要再访问pos地址中的成员时,还要再加一个->才行,所以本来应该是it->->_row。但是这里是it->_row,直接一个箭头就访问到了pos的成员。

这是为了语法的可读性,编译器做了特殊处理,自动帮我们省略掉了一个->。
以后的map、智能指针等也会有这样的operator->的重载,用的时候都是自动省略一个箭头,只要记住,->返回的是数据的地址,也就是原生指针,原生指针加->就访问到了成员。

前面说的都是非const的对象,那么const的对象迭代器怎么搞。

const迭代器

非const对象是可以调用const成员的,但是非const对象就不能调用非const的成员了。

现在定义如下函数,功能是遍历链表:
【C++】手把手教你模拟实现list的基本功能_第28张图片
参数给的是const &的,但是这是出问题了,我们前面没有实现const对象的beign和end。
给出如下错误的beign和end:
【C++】手把手教你模拟实现list的基本功能_第29张图片
此时虽然能遍历了,但是是有问题的,我们可以通过其中的迭代器修改const链表。
【C++】手把手教你模拟实现list的基本功能_第30张图片
【C++】手把手教你模拟实现list的基本功能_第31张图片

【C++】手把手教你模拟实现list的基本功能_第32张图片
此时不会报错,但是其实不是我们想要的。
那么我们就还要实现以下const的迭代器。

怎么实现呢?
有的同学说可以将非const的代码拷贝一份再改为const的代码。
可以是可以,就是比较麻烦。
我们看一下STL库中是怎么实现的:

迭代器中:
在这里插入图片描述

链表中:
【C++】手把手教你模拟实现list的基本功能_第33张图片
可以看到,库中list类中重命名迭代器的时候多了两个模板参数。分别是引用和指针。
有什么用呢?
再看__list_iterator的模板参数有三个,T、REF、PTR。
答案揭晓,当我迭代器传参为非const的T、T&、T时就会实例化出非const的迭代器,当我迭代器传参为const的T、T&、T时就会实例化出const的迭代器。

那么就好说了,我们也把这些模板参数改改。
__list_iteartor中改一下模板参数,并把所有的T*改为Ptr,把所有的T&改为Ref:
【C++】手把手教你模拟实现list的基本功能_第34张图片

list中改一下iterator的typedef并加上const_iterator的begin和end:
【C++】手把手教你模拟实现list的基本功能_第35张图片

然后const的就能跑起来了:
【C++】手把手教你模拟实现list的基本功能_第36张图片
这里是一个难点,不懂的同学好好钻研钻研。主要是const和非const迭代器中每个函数的返回值要搞清楚是为什么那样写的。

list::insert

插入,在pos的前一个位置插

【C++】手把手教你模拟实现list的基本功能_第37张图片

测试:
【C++】手把手教你模拟实现list的基本功能_第38张图片
但此时若用算法库中的find会报错:
【C++】手把手教你模拟实现list的基本功能_第39张图片
因为我们iterator的实现不符合STL的规范,需要typedef一堆东西,加到__list_iterator中。
STL龟腚一个规范的迭代器要加上一些东西:
【C++】手把手教你模拟实现list的基本功能_第40张图片
加上了之后就能用库中的find了。
【C++】手把手教你模拟实现list的基本功能_第41张图片

看:
【C++】手把手教你模拟实现list的基本功能_第42张图片
我们可以用insert来复用一下push_back和push_front。

insert复用的push_back

【C++】手把手教你模拟实现list的基本功能_第43张图片

insert复用的push_front

【C++】手把手教你模拟实现list的基本功能_第44张图片

测试一下:
【C++】手把手教你模拟实现list的基本功能_第45张图片

再写一下erase。

erase

代码如下:
【C++】手把手教你模拟实现list的基本功能_第46张图片

测试一下:
【C++】手把手教你模拟实现list的基本功能_第47张图片
–end是因为,end()是哨兵位头结点,不能删除,所以要–一下,变为最后一个有效结点。

【C++】手把手教你模拟实现list的基本功能_第48张图片

再借用一下find:
记得list的erase会出现迭代器失效的问题:
【C++】手把手教你模拟实现list的基本功能_第49张图片
上面的代码崩掉了。
不要多次重复删除同一位置的迭代器不修改。不然就是野指针。
库中的和我这里的实现都是要将删除的后一位返回。用上这一点就不会崩掉。
【C++】手把手教你模拟实现list的基本功能_第50张图片

然后还可以用erase来复用pop_back和pop_front。

erase复用pop_back

代码:
【C++】手把手教你模拟实现list的基本功能_第51张图片

测试:

【C++】手把手教你模拟实现list的基本功能_第52张图片

erase复用pop_front

代码:
【C++】手把手教你模拟实现list的基本功能_第53张图片

测试:
【C++】手把手教你模拟实现list的基本功能_第54张图片

就剩下一些尾没收了,马上结束,看到这的再坚持一下。

析构

前面我们是没有写拷贝构造的,析构也没写。
【C++】手把手教你模拟实现list的基本功能_第55张图片
上面没有拷贝构造,就会出现浅拷贝的问题,但是代码运行起来没有崩掉,因为我们还没有写析构,默认的析构是不会释放我们开的空间的。

浅拷贝如下图所示:
【C++】手把手教你模拟实现list的基本功能_第56张图片

如果我们此时手动释放空间就会崩掉。
但是我们写析构之前要写一下clear函数,这个是stl库中有的,功能就是将链表中的结点清空,只留下哨兵位头结点。

list::clear

代码:
【C++】手把手教你模拟实现list的基本功能_第57张图片

测试一下:
【C++】手把手教你模拟实现list的基本功能_第58张图片

然后再复用一下clear来写析构。

【C++】手把手教你模拟实现list的基本功能_第59张图片

再测试一下,光标闪一下,就崩掉了:
【C++】手把手教你模拟实现list的基本功能_第60张图片

这里归根结底的问题还是默认的拷贝构造导致浅拷贝了,所以我们要重新写一下拷贝构造。

拷贝构造

只要这个类有动态开辟空间的成员就都是要写这个的。不然就有浅拷贝的问题。

传统写法,我们可以挨个遍历链表,一个个的push_back。
但是比较麻烦,我们可以复用一个构造函数,就是迭代器区间构造。
用这个迭代器区间构造来造出来一个链表,然后再将该链表与原链表交换。

库中是有这个迭代器区间构造的函数的。

所以先实现一下迭代器区间构造:

迭代器区间构造

先把要构造的链表初始化,然后再将构造的数据一个个push_back就行。
如果不初始化就会崩掉,因为插入第一个时要用到头结点,但是没初始化,next和prev都是指向未知的。

先写一下初始化的函数,其实就和无参构造一样。
【C++】手把手教你模拟实现list的基本功能_第61张图片
写了这个之后无参构造里面直接调用init就可以了。

迭代器区间构造:
【C++】手把手教你模拟实现list的基本功能_第62张图片

测试一下:
【C++】手把手教你模拟实现list的基本功能_第63张图片

然后我们复用一下迭代器区间构造来实现拷贝构造。
先初始化(不初始化也会崩),再构造出来一个对象,然后进行交换。
不初始化会崩掉是因为最后交换的时候,将未初始化的数据交还给了tmp,然后tmp生命周期只在list这个构造函数中,调用结束,tmp调用其析构,delete了未开辟的空间。
【C++】手把手教你模拟实现list的基本功能_第64张图片

【C++】手把手教你模拟实现list的基本功能_第65张图片
下面这个swap中的::swap是库中的swap。
交换一下二者的哨兵位头结点就好了。

然后就是赋值运算符重载了,也是默认的不行,会浅拷贝,所以要自己写一下。

【C++】手把手教你模拟实现list的基本功能_第66张图片
上面的是简便的写法,ls为拷贝,拷贝完了再将原来的数据与其交换。

测试一下:
【C++】手把手教你模拟实现list的基本功能_第67张图片

最后说一个东西。

类内用类模板名可不加模板参数

例如:
【C++】手把手教你模拟实现list的基本功能_第68张图片
一般情况下都是要加的,但是只要在类内,其自己的类内,就可以不加模板参数。

再比如说迭代器中的:
【C++】手把手教你模拟实现list的基本功能_第69张图片
ListNode是类外的,不能省略。
但__list_iterator是类内的,所以可以省略。

但是建议还是都加上。
【C++】手把手教你模拟实现list的基本功能_第70张图片

最后这一点不做强求,只是让大家了解一下,以后遇到了不懵B就行。

到此结束。。。

你可能感兴趣的:(c++,list,算法,数据结构,c语言)