list的底层逻辑是一个双向带头链表。那么list的底层其实就跟我们之前实现的带头双向链表相同,都是开辟一个一个单独的节点,最后再通过指针将各个单独的节点链接起来即可。
我们来类比之前编写的双向带头链表实现具体的内容。
创建一个list类的主体
就像我们说的那样,我们需要先创建一个带头的空链表。在编写list的构造函数之前我们需要先实现一个结构体,一个用于创建节点的结构体。之后我们可以直接new一个该节点直接进行链接操作即可。
我们创建一个结构体之后就可以在里面再编写一个构造函数,用于初始化我们节点的内容,在创建的同时可以对节点进行初始化操作。
那么之后我们就可以进行编写list的构造函数了:
我们在默认构造当中进行的操作就是开辟好一个头节点,并将该节点单独链接成为带头双向循环链表。
push_back()尾插函数
在初始化好链表之后我们就可以进行链表功能的正式编写了。其实插入操作也非常的简单。我们只需要新创建一个节点,并将该节点链接进入带头双向循环链表当中即可。
我们仔细梳理逻辑可以发现:想要实现尾插操作只需要在头节点之前链接进入新的节点即可。因为我们的头节点的前一个位置就是我们规定链表的最后一个位置。编写代码如下:
iterator类
之后我们就会想要测试该函数是否可以正常的运行。所以我们就需要一个可以打印出链表当中元素的函数。所以我们下一步就需要实现我们的迭代器的功能。
但是我们会发现,list的迭代器跟我们之前编写的vector和string的迭代器有很大程度上的区别。之前编写的迭代器都是顺序存储结构,我们可以使用地址的指针作为迭代器直接使用。但是list是链式存储结构。那么就不能直接使用地址作为迭代器了,所以我们封装了一个新的类作为迭代器。
在这个迭代器当中我们创建了一个新的node结点用于在链表当中进行移动。(因为我们原有的类list类当中_head指针不能进行改变,所以就需要一个新的node指针适应我们的++操作。)之后我们只需要实现相应的构造函数即可。
仔细看的话大家肯定会对iterator的构造函数有疑问,这不是进行浅拷贝了吗?那么在释放的时候会不会有歧义呢?在这里确实进行了浅拷贝操作,但是我们想要的就是浅拷贝的效果。因为我们迭代器想要指向的就是我们已经开辟好的地址的指针,所以进行浅拷贝才是我们想要的效果。
我们可以通过使用库当中的迭代器打印输出链表数据的形式进行反向推导我们想要实现的相关的功能。
首先我们需要实现的就是begin和end函数。我们需要返回一个迭代器类型的数据。
让我们进行分析一下:我们只需要返回_head指针的下一个位置即可,因为_head指针的下一个位置表示的是链表当中第一个元素节点的指针,作为返回值我们系统会自动调用iterator的构造函数,参数类型正好相同直接会生成一个对应的_node节点,也就是我们作为变量持续进行循环的对象。
同样的end函数也是这个道理。我们只需要返回_head指针即可。
之后我们需要实现的功能就变成了三处:1.实现 != 2.实现 * 3.实现 ++
运算符重载*
对于*的运算符重载函数其实很简单,我们想要的就是打印输出链表当中的数据内容,所以我们只需要返回链表当中存储的数据即可。也就是我们迭代器当中保存的节点_node当中所对应的值。
运算符重载!=
我们可以思考一下不等号两边的数据类型。
仔细观察我们就会发现,不等号两边都是我们重载之后的iterator的类型,而我们想要比较的是迭代器当中保存的节点的指针。所以我们可以直接写出如下的代码:
第一个_node是我们的左操作数tmp当中的成员变量,我们可以直接使用,而我们的右操作数是一个iterator的类型的数据,所以我们就需要通过 . 操作符找到当中的成员变量。这两个参数就是我们想要比较的对象,我们直接比较并返回比较之后的结果即可。
运算符重载++
对于++的运算符重载函数同样很简单,我们只需要修改iterator当中的node节点当中保存的数据即可。也就是将_node指针指向下一个位置即可。
在实现上述的函数之后,我们就可以打印输出链表当中相应的数据了。
iterator的模板参数
T&和const T&
当我们在使用链表进行数据输出的时候很容易会想到:list当中保存的是变量数据,但是如果我们想要保存常量数据,也就是不允许其他人修改链表当中的数据应该怎么做呢?
我们进行分析可以发现:
实际上起作用的也就是我们的解引用操作,如果我们对T进行一个限定也就是添加一个const进行修饰不就好了吗?那该如何做呢?直接typedef const list_iterator<T> const_iterator ? 当然不行我们会发现这个const是针对于我们的iterator类并不是仅仅针对于我们的T参数。那么就有人可能会说了:直接复制一份命名为const_list_iteartor,并将模板传承const T不就好了吗?但是我们会发现我们就仅仅修改了一个参数,而我们的代码却变得十分冗余。为什么不直接添加参数呢?
T*和const T*
我们只需要添加一个参数进行修改即可。对于不同参数的迭代器类型我们可以命名成不同的形式,之后系统会自动实例化成为相应的类。如果我们调用的是普通的迭代器就会实例化成为T*,如果传入const迭代器就会实例化成为const T*,从而达到我们想要的效果。
当我们的链表当中保存的是内置类型的数据上述功能已经可以正常使用了,但是如果我们的链表当中保存的是自定义类型的数据呢?我们会不会想到需要重定义一个 -> 方便我们进行查找自定义类型当中的变量呢?
如果我们不进行重载 -> 操作的时候我们必须进行上述的操作。但是我们如果重载 -> 之后我们就可以通过该操作符直接进行打印操作。
是不是看起来简洁很多呢?那么我们也将我们的函数进行实现该功能。
实质上就是通过->之后我们可以获得链表节点当中保存的数据的地址,如果该数据是自定义类型我们就可以通过这个指针找到其中的成员变量。
但是我们会发现有一点奇怪:按照这么说我们应该进行的是两次 -> 呀?事实上确实是两次操作,第一次->是我们的运算符重载,会获得自定义类型的指针,第二次->找到自定义类型当中的成员变量。
同样的 -> 在使用的时候也会有const的需求,那么我们就可以按照之前的思考,再添加一个参数,用来控制我们的T*是否可以被修改。
修改代码并运行之后发现结果一切正常。
insert函数
实现完成上述代码之后,我们list的大部分逻辑都已经完成了,那么接下来就可以进行相关具体功能性函数的添加操作了。首先我们来实现insert函数。
我们只需要开辟一个空间,将新开辟的空间链接进入链表当中即可。
erase函数
对于删除函数,我们只需要取下某个节点,链接前一个节点跟后一个节点即可。但是我们需要注意的是返回值的设置。由于我们的该位置的节点已经释放,所以我们该位置就无法使用,所以为了避免迭代器失效的问题,我们需要主动传出一个返回值,该返回值是我们删除节点的下一个位置。
代码运行一切正常。
拷贝构造
之后我们来实现拷贝构造函数。实现拷贝构造函数,首先我们需要获得头节点,由于构造函数都需要执行该功能,所以我们将获取头节点的函数独立封装成为createhead函数方便我们进行复用。
之后只需要将我们原链表当中的元素依次尾插进入我们的新链表当中即可。
代码运行一切正常。
运算符重载=
对于=的运算符重载同样很简单,我们只需要交换头节点即可。释放的工作完全可以交给析构函数自动进行。
clear函数
在完成析构函数之前,我们先完成以下clear函数。该函数的功能是清空链表当中保存的值,但是不释放头节点。我们可以重新进行对链表的操作。
对于该功能的实现,我们直接复用之前实现的erase功能即可。但是需要我们注意的是erase之后我们需要使用it接受删除之后的指针,否则就会产生报错。
运行结果一切正常。
析构函数
析构函数我们直接调用clear函数清空链表当中的节点,最后再释放head节点即可。
退出码为0,代码运行一切正常。
使用n个相同的参数进行构造
原理还是相同的,我们只需要初始化头节点之后尾插n个由该数据构造的节点即可。
代码运行一切正常。
迭代器构造
首先我们需要进行的是初始化头节点,之后依次拷贝迭代器区间的值即可。
同样的当我们使用模板参数初始化迭代器的时候需要重载一份使用n个参数进行构造的构造函数,避免重复,避免适配错误。
size函数
这个函数用于返回我们链表当前的元素的个数,我们可以避免麻烦直接创建一个成员变量,当我们对链表当中的数据进行修改的之后产生对应的改变即可。
代码运行一切正常。
front函数和back函数
该函数的功能就是返回链表当中的第一个位置还有最后一个位置。我们分别实现普通版本和const版本。
代码运行一切正常。
头删,头插,尾删,尾插函数
尾插函数我们已经实现完成了,那么之后我们只需要完成头删头插,尾删操作即可。
代码运行一切正常。
重载运算符--
重载运算符--操作其实跟我们重载运算符++操作很像,我们只需要将移动的指针修改成为prev即可。
==运算符重载
同样的道理其重载过程跟我们的!=完全相同。
那么到此位置list的底层逻辑实现也就全部完成了。