目录
目录
前言?
一,STL -- List 的基本认识?
1.1基本认识
1.2 list:双向迭代器
1.3一些list特有接口
二,模拟实现
2.1节点实现
2.2正向迭代器模板实现
2.2.1list迭代器:非原生指针的认识
2.2.2指针和引用的实现
2.2.3 operator++、--?操作实现
2.2.4 operator == ,!= 操作实现
2.3f反向迭代器模板实现
2.3.1基本设计
2.3.2指针 T*?和解引用 T&
2.3.3?前置++,--
2.3.4operator=,!=实现
2.4 list 模板实现
2.4.1基本设计
2.4.2实现 list 迭代器
2.4.3构造析构
2.4.4拷贝构造,赋值重载
2.4.5 现代写法实现拷贝构造和赋值重载
2.4.6 插入删除
三,总结
前言?
小伙伴们大家好啊!本文继续为大家带来有关STL容器相关的内容 -- list。list也就是链表。那么相对于顺序表表示的vector等容器来说,链表也是有自己的优点的。比如vector最大的缺点,头插和中间插入的复杂度是O(N),以及可能会浪费空间,而list最大的有点也就是头插和中间位置插都是O(1)。所以对于list的认识,就变成了必不可少的内容了。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
一,STL -- List 的基本认识?
1.1基本认识
那么对于基本的内容,string,vector 和 list 都差不多,为了方便学习理解。所以这里我们不再过多赘述。但是在 list 中也有一些和string,vector不同的内容。
1.2 list:双向迭代器
如下图所示,可以看到,它是一个 bidrectional 双向迭代器。
同时我们需要知道的,在所有的链表中,最优的一定是带头双向循环链表。而在STL中,list 的底层就是带头双向循环链表。所以本文依旧是全部按照STL源码展示的带头双向循环链表实现。
而在C++中,是有这样一些迭代器的,如下图所示,首先是 InputIterator 和 OutputIterator,他们是最基础的迭代器,后面有 forward_Iterator 单向迭代器,也就是我们最常用的可以理解为就是指针一样的事物。再往后就是双向迭代器了,而对于双向迭代器而言,类似于指针,但是那远远复杂于指针。再往后就是随机迭代器了,那么如果要支持随机访问了,那么就比如vector就是一种随机迭代器了。
那么在后续的模拟实现中,我们会对于当前接口的双向迭代器有一个更深层次的了解。
1.3一些list特有接口
首先是表示链表第一个节点的 front 函数和表示链表最后一个节点的 back 函数。
但是对于这两个函数来说,用的并不是很多。
其次就是下面一些函数:
其中,swap函数可以实现两个链表的交换。
splice可以实现链表,一段链表或者空链表的连接。
而unique可以去重,将list中只留下互不重复的元素。
而sort就是实现链表中所有元素的升序排序。而且可以看到sort是使用了一个仿函数,而该仿函数默认就是升序比较排序的。那么对于仿函数我们也将在后面模拟实现具体了解他的作用以及实现。
二,模拟实现
好的,那么经过上面的基本了解。对于list的基本使用来说,应该是基本没有什么问题了,但是知其然,需知其所以然。为了更深层次的了解list的实现,接下来我们按照 stl 源码模板,模拟实现一下 list。
2.1节点实现
首先,需要一个节点。该节点是一个带头双向循环链表,所以有两个指针。同时对于一个节点而言,我们应该将其初始化,否则后面创建节点的时候调用构造就会出错。
2.2正向迭代器模板实现
2.2.1list迭代器:非原生指针的认识
前面我们有说过,对于 list 而言,它的迭代器并不是一个原生指针类型的迭代器;而像vector就是一个原生指针,所以vector的迭代器实现就比较简单了,可以直接将指针进行简单的封装即可使用。但是对于?list 而言,就需要使用其他方式封装了,可以简单理解如下:
2.2.2指针和引用的实现
所以首先,将正向迭代器实现如下:
那么首先我们需要使用指针和引用,所以首先将引用表示的节点的内容封装在 operator* 中;接着将节点的地址封装在 operator-> 中。
2.2.3 operator++、--?操作实现
接着需要实现 operator++ 的前置++和后置++,以及operator-- 的前置和后置。但是因为他们的大概原理是一样的,这里只实现 operator++:?
那么可以看到的是,我们通过一个形参实现了前置++和后置的区别,因为后置++返回的是原先的值,所以事先使用 tmp 将该对象拷贝下来,然后返回。
2.2.4 operator == ,!= 操作实现
那么对于这样的一个迭代器的比较而言,我们能比较的只有节点是否相等:
2.3f反向迭代器模板实现
上面实现了有关正向迭代器相关的内容,但是因为 list 的迭代器是一个双向迭代器,所以对于反向迭代器,我们依旧需要实现。
根据源码我们可以发现,那么对于反向迭代器而言,为了和正向迭代器保持一致,所以反向迭代器的 起始 和 结束 位置时正好相反的。也就是入上图所示的位置显示。
2.3.1基本设计
因为前面已经实现了正向迭代器了,所以直接使用正向迭代器实现反向迭代器。所以对于反向迭代器而言,所以的内容都是通过正向迭代器实现的。?当然这里就包括了指针,操作符的重载以及所有的函数实现。
2.3.2指针 T*?和解引用 T&
为了和正向迭代器保持一致,反向迭代器的 rbegin 指向的是空位置的头节点,所以不管是指针还是解引用都是表示它的后一个位置的元素。
2.3.3?前置++,--
下面实现的是前置 ++ 和 前置--,后置一样的道理。不过这里区别前置是通过引用解决的。但是需要知道这里的 ++ 是往后走的,而 -- 才是往前走的,因为它是反向迭代器。
2.3.4operator=,!=实现
因为是通过正向迭代器实现的,所以是直接通过两个迭代器的比较实现。
2.4 list 模板实现
2.4.1基本设计
如上图所示,我们将上面实现的三个模板:节点模板,正向迭代器,反向迭代器。都重命名一下,然后就可以使用他们的简化名字了。
2.4.2实现 list 迭代器
那么经过上面的封装,接下来就可以直接将 list 类的两种迭代器都实现了,那么对于const 迭代器而言,其实道理是一样的,所以这里只实现普通的迭代器。
同时可以看到这里反向迭代器是和正向迭代器正好相反的。
2.4.3构造析构
对于构造和析构而言,因为 list 类的变量只有一个那就是节点的指针,所以构造和析构的实现是比较简单的。
2.4.4拷贝构造,赋值重载
因为默认的构造函数实现的是浅拷贝,所以会有很多问题,所以我们也需要自己实现。
那么首先需要了解的是传统意义上的写法,也即按规矩办事。对于拷贝构造而言,首先将头节点初始化,然后再依次将每个元素进行尾插。
可以看到这里我们是通过范围for实现的,那么因为范围for的底层是迭代器,所以说明迭代器也是不可或缺的。?
然后就是赋值重载了,可以看到首先第一点,因为我们要保证该对象并不是自己给自己赋值,所以需要判断,然后就是为了避免赋值错误,首先我们将?this 对象清空。最后再依次尾插,然后将this对象返回。
2.4.5 现代写法实现拷贝构造和赋值重载
如下图所示,那么首先, 这里我们是将通过迭代器构造的形式构造出了一个临界对象,然后再将该对象的头节点与 this 对象进行交换,那么当该函数调用结束的时候,临时对象的生命周期结束了,它会自动调用析构函数将自己释放,那么就达到了“一石二鸟”的效果。
同时最重要的,赋值重载的实现就简单多了。因为我们可以通过拷贝构造的临时对象 -- 即形参,与this对象的头节点进行交换。同样的调用结束,临时对象的生命周期结束。自动调用析构函数。非常的简单方便。
在上面现在写法中,实现了有关迭代器构造一个对象的内容。但是因为它的实现,和传统的构造是是同样的道理,所以这里不实现了。
但是需要注意,如果有一种对象的定义是这样的:list<int> lt (2,3);它的本来意思是,构造一个对象,有两个元素,每个元素的大小是 3。但是因为我们之前实现的构造函数一个参数是无符号整型,另一个参数是 T 类型,所以它就会认为迭代器版本的构造函数,因为它的两个参数都是一样的,是Iterator,所以就会匹配迭代器版本的,那么就导致结果出错。
所以结论是,我们需要额外再实现一个匹配的构造函数,也就是将 size_t 改为 int即可。?
2.4.6 插入删除
那么对 list 而言,有头插,尾插,指定位置的插入,删除也是一样,但是如果我们实现了指定位置的插入和删除,那么头尾的插入删除直接复用即可。
所以首先指定位置的插入:因为是双向链表,所以直接将新节点,迭代器位置节点,上一个节点之间的关系链接在一起。最后再将节点强转后返回即可。
那么因为对于插入而言,因为插入之后,是在迭代器位置插入了一个元素,所以迭代器表示的元素并没有发生任何改变,也就是迭代器依旧是有效地。但是如果是删除,那么当前迭代器表示的节点就被删除了。所以当前迭代器一定会失效。
如下所示:可以看到当记录了迭代器节点的前后节点之后,直接将迭代器节点删除了,所以该迭代器一定是失效了。那么就需要额外关注,否则如果再次使用该迭代器表示的节点,那么就是内存泄漏问题了。
三,总结
? ? ? ?首先我们了解了 list 的基本内容,因为 list 的迭代器不同于 vector 等,不是一个原生指针类型,因为 list 容器的元素地址不一定是连续的,所以不能通过原生指针的简单运算实现。而是需要将原生指针和链表的结构封装一下,最后得到的便是一个封装后的迭代器。同时因为是双向迭代器,所以我们将反向迭代器同样也实现了。需要注意就是为了保持一致,反向迭代器和正向迭代器正好是相反实现的,所以是将正向迭代器进行了封装。 ????????然后就是模拟实现的时候,需要注意迭代器失效的问题。以及在将拷贝构造和赋值重载通过现代写法实现的时候,需要用到迭代器版本的构造函数,那么就可能会导致一些匹配问题。
好的,那么对于 list 的实现,到这里就全部了解完了。如果哪里有问题,还请大佬们提出来哦!
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
|