在之前list的使用中发现list接口的使用和string及vector是类似的 这也体现出stl封装这些数据结构的意义 熟悉一个数据结构的使用方法后 其他的也很容易就能上手使用
本文来详细实现一下c++里面list的底层结构及遇到的很多问题 list的实现区别于之前的vector主要问题其实就是迭代器的问题
string和vector因为天生物理结构是连续的 所以在实现他们迭代器的时候就就可以直接用类型指针重命名为迭代器 但是list物理空间上不是连续的 那list的迭代器为什么能和string及vevtor一样使用呢 list迭代器底层是如何实现的能支持这样的功能
----------------------------------------
在实现之前先说一下按需实例化的问题
在模版里面编译器检查不严格 只会检查简单的问题 比如少了;() 这种问题
很多的越界或者调用不存在的函数这样的问题不会检查 只有在调用的时候实例化那个函数之后 才会报错 所以我们在写的时候可能有很多的问题了已经 但是没有给我们报出来 等我们使用的时候会一下子报很多错出来 要注意一下
首先在写list真正结构之前 需要先写一个list_node类型的类 来存一个节点的信息(数据 及前后位置的指针) 然后写一个含缺省值的构造函数 (这里缺省值的方式之前vector解释过 这样子主要是为了支持自定义类型的缺省值) 把前后指针都初始化为空指针

list的类 成员变量就是刚刚写的节点类型的指针 head(哨兵位) 这里为了方便额外写的size 这里因为要频繁使用到刚刚的节点类 将其重命名为Node方便使用
这里实现的无参默认构造函数 为头结点申请了一个节点的空间 然后让头结点的前后指针都指向自己

然后实现尾插
尾插要先给要插入的新节点newNode申请一个空间 插入之前要先保存一下插入之前的最后一个元素的地址到prev 然后断开之前最后一个元素和头结点的连接 然后让将新节点和插入前的最后一个节点和头节点连接 prev的_next指向newNode newNode的_prev指向prev head的_prev指向newNode newNode的_next指向head

迭代器的实现
在list里面实现迭代器和之前的string及vector的实现有很大的区别
因为string vector是顺序表的结构 物理空间上是连续的 之前实现的迭代器就可以直接是类型的指针 解引用之后就是当前位置的元素 ++之后就是下一个元素的位置
但是对于lsit 因为1.单独封装一个节点类Node 解引用之后是当前指向的节点 要访问值里面的值还需要用 ._data的方式访问元素 2.物理空间上不连续 所以++之后不知道到哪一个位置了 不是我们所想的下一个位置的地址
如下图 想使用list迭代器的效果 发现不能使用

所以list迭代器的实现需要想办法来支持我们想要实现的效果
解决的方式是给迭代器封装一个类 里面的成员变量只有一个节点类型的指针 然后在里面用运算符重载的方式让它对外实现我们想要实现的效果

迭代器在对外使用上我们其实就可以把它当做节点类型的指针 (里面的唯一成员变量就是节点类型的指针 在类里面对这个成员变量通过运算符重载的操作 类型对外的表现上像指针一样 且实现的功能由里面的重载决定)
例如接下来对解引用操作符* 及++ == ->进行重载
这里的解引用操作我们期望的对外表现为 解引用之后是该节点位置的值date 实现类内运算重载要以类里面的成员变量_node来考虑 _node此时是节点的指针 解引用之后就是当前节点 然后._date 就是当前位置的值了 所以直接返回(*_node)._data
这样对外我们将迭代器使用的时候 用解引用操作就可以直接访问到了里面的值date
而++对外我们的期望功能是到达下一个位置的地址 当前位置的节点里面存着下一个位置的地址在_next中 所以 对于++重载 我们直接让_node指向下一个位置的地址 _node=_node->_next
这里的== !=重载 我们不是要对里面的值_data进行比较 重载是根据我们的需求来重载的
在之前的使用中我们知道 用迭代器方式打印结束的条件就是等于end也就是最后一个位置的迭代器 那么他们判断的方式就是比较节点的指针 所以这里就用比较指针的方式实现 也就直接比较_node

在重载这些运算符之后 并在list中写了begin和end 简单的迭代器功能就可以正常使用了
begin返回的是第一个元素的位置 也就是头结点的后一个位置 end返回的是最后一个元素的后一个位置 那么也就是头结点
iterator只是对外使用我们可以当做类型指针来直接使用 但是在实现功能的时候要注意 迭代器iterator是一个自定义的类
返回值是iteraotr 所以可以创建一个iterator类型对象 用对应的指针类型初始化传回去就可以 当然匿名对象的方式或者隐式类型转化都可以

这样就可以使用迭代器的方式打印了 而底层就是迭代器的范围for打印方式也可以了

解引用重载的返回结果是T(模版)类型 对于内置类型就是数据了 那么如果这里的T是自定义类型呢 那么解引用之后是这个自定义类型 用访问里面的元素还需要.自定义成员变量名 的方式来访问里面的元素
那么我们是不是可以把->也给重载了 让它实现我们所期望的功能 访问到自定义类型里面的数据
但是->的重载很奇怪 返回值是T*类型 也就是自定义类型的地址 为什么这样呢

结合解引用重载来理解一下
如果T是内置类型_node是Node类型的指针 在解引用之后就是内置类型的date 所以直接返回的是(*node)._data

那如果T是自定义类型的AA 用->的操作我们期望访问的就是AA类里面的_a1或者_a2 但是在重载->中我们不知道这个自定义类型的成员变量是什么名字(不同的自定义类型里面的成员变量名字不确定) 所以不能直接返回自定义类型的成员变量

但是为什么就采用的是如下返回T指针的方式

这样不是离我们所期望的更远了吗 却可以按我们预期的方式来实现 如下图

其实这是因为还隐含了一个-> 真正情况其实是下面这样 it调用operator-> 返回了T* 这里就是AA* 然后对于AA*又调用了一个原生的-> 然后访问到了_a1和_a2 那么为什么这样设计呢 没有别的原因 就是为了可读性 连续写两个->看着不美观
虽然只写一个-> 真正的情况就是两个-> 第一个是重载的-> 第二个是原生的

然后顺便把后置的++及 --都给重置了 后置的需要先存处理前的_node做返回值

然后实现下insert
insert也很简单 和尾插没什么区别 给新的节点开了空间后 存节点然后改变相应指向就可以了
再强调一下 迭代器的封装使得我们在使用迭代器的时候可以根据我们的设计对外实现期望的功能 但是在功能的实现上我们要以里面的_node来进行 这里通过_node才能找到前一个和后一个位置的指针 真正的iterator是一个自定义的类型不能找到前后位置的指针

其实写了insert之后 尾插和头插直接赋用insert就可以了

erase的实现及头删尾删和insert类似

之前在vector实现过下面的打印函数 任何容器都可以使用 里面用到的是范围for 但是当我们用该函数打印的时候会报错 而之前我们实现迭代器之后范围for可以正常使用没有出现过这样的问题 为什么呢


这是因为这里的函数形参是const修饰过的 范围for的打印底层用到的是迭代器
如下图 对于左边的list类型的容器 l 可以调用普通的迭代器实现范围for 但是如右边使用打印函数的时候 l传给形参con con是const修饰过的 表示容器里面的内容不能被修改 它不能调用普通的迭代器不然返回的是普通的itertor权限放大了

在之前我们使用的时候
const类型的迭代器是用cosnt_iterator(整体是一个类型)的方式 那为什么不直接用const iterator的方式呢
这里和const修饰指针类似 如下图 cosnt在不同的位置修饰的内容是不同的

这里的迭代器也是一样的道理
const_iterator 表示指向的内容不能改变 const iterator表示迭代器本身不能改变
所以我们还需要实现const_iterator
const_iterator 相较于iterator主要是让里面某些运算符重载返回值用const修饰 保证不会改变里面的内容 对于这里主要就是对解引用重载的返回值和->重载的返回值用cosnt修饰
this用const修不修饰其实无所谓 权限可以放大不会报错 不用const修饰this相当于在重载函数中可以修改容器里面的内容

写了两个迭代器之后 const类型的对象就调用const类型的迭代器 返回的就是const类型
普通对象可以调用普通的也可以调用const的 但会优先匹配适合自己的
如下const类型对象调用会调用const类型的begin返回值为const_iterator给it 所以it容器里面的内容不能改变 在下面*it+=10的操作就会报错(这里是模版 还是只有调用时候把它实例化才会报错 实例化之前里面简单的错误才会报错)

在这样写了const类型迭代器之后 刚刚在打印函数中不能使用的范围for打印就可以正常使用了
但是iterator和const_iterator里面内容大部分都类似 只有解引用的重载和->的重载的返回值有区别 所以为了const_iterator的实现重写一个const_iterator类是不是有些冗余了
stl中list的实现对于这里就用到了一种很巧妙的方法解决这样的问题
如下图

下面是普通迭代器的情况 普通对象调用普通的begin返回iterator 然后会实例化出模版里面第二个参数为T& 第三个参数为T* 的情况 然后*和->重载返回值就是相应的类型

const类型的对象调用cosnt类型的begin返回const_iterator同理

这样的方式实现只需要一个类就可以实现 但是其实这种方式和刚开始用两个模版类实现的方式底层一样
前面的是写好了两个类模版 到时调用哪个就会实例化哪个类模版 而这种方式是写了一个模版 当需要哪个时候就会通过这个模版来实例化出需要的类
打印中如果用迭代器方式 it的类型前面如果没有加typename会出问题(这也是vector那么遇过的 在函数模版中 调用一个类里面的东西 可能是类型可能是静态成员变量 编译器不知道具体是什么 需要我们用这种方式来指定)

然后实现一下析构 拷贝 赋值重载
析构就是要把全部节点给挨着一个个释放 本身list类里面也需要有clear 那就写了clear然后直接调用clear就可以
在clear中用迭代器方式遍历链表然后挨着删除 这里要注意每次删除之后迭代器的位置要更新(之前指向的位置被释放掉了 之前实现erase返回的就是下一个位置的迭代器) 这里注意不能用i++的方式 i指向的位置已经被释放了 i++是我们自己重载实现的 我们知道++是通过_node=-node._next方式实现的 而此时的_node已经被释放掉了已经
在析构中调用clear之后还有一个头节点没有释放 然后释放掉头节点
在写了析构之后 如果我们没有写拷贝构造直接用默认的话就会崩 这还是之前遇到很多次的浅拷贝问题同一块空间会释放两次 所以拷贝构造需要我们自己写来实现深拷贝
如下 拷贝构造形参要是自己类型的引用必须(忘写了 如果没写会引起无穷递归) 里面实现的时候直接用范围for的方式遍历形参ll 把它的每一个里面的数据直接尾插 不过在这之前还需要先给头节点开空间并初始化next和prev指针指向自己

赋值重载和拷贝构造类似默认的也是浅拷贝同一块空间释放两次会崩溃同样需要我们自己来实现
如下 赋值重载这里还是用之前的现代方法实现


如下图形参ll通过实参l2进行拷贝构造实现 然后直接交换形参和l3的的内容 但是这里的swap由我们自己实现 否则算法库中的swap对于自定义类型需要进行三次拷贝构造代价有些大 而我们自己实现的swap是直接交换里面存的指针_head和_size


最后再说一个c++11中出现的对于vector和list初始化的一种方式
在c++11中库中的list和vector支持下面这种方式来进行构造 这样是怎么实现的呢

c++11里面有了一个initializer_list 这个类型可以直接用{ }这样的方式来初始化 它也是一个模版


在写了下面这样的构造函数之后我们也可以支持这样的使用方式了

其实就是下面的 {1,2,3,4,5,6}先隐式转换为initializer_list类型然后进行了上面这样的构造函数 在同样先搞个头节点 然后用范围for的方式把里面的内容尾插一下


168

被折叠的 条评论
为什么被折叠?



