目录
1.观察库中的链表的实现
首先一上来就是一个结构体模版 , 此处库中将void_pointer(也就是指向自己的指针)设计成一个void* , 所以在之后的使用中必定涉及到强转来使用,我们更建议直接设计成T*
然后是一个超长的和iterator有关的结构体模版的定义:
(此处没有截图完整,在压缩包中48~94行)
由此我们可以推测,双向迭代器是一个类封装的,不再是vector中简单的原生指针。
最后来到链表,通过观察我们可发现,主要成员变量就是一个node:
而node的类型是一个节点的指针
初始化:无参构造就是实现一个哨兵位,哨兵位的next和prev都指向自己
get_node:通过内存池(为了效率)或得空间
其它插入方法:
push_back 以及 insert
都是将原来的元素往后顺移一个,将希望放入的元素放在position的位置
我们还能观察出,position被当作一个指针一样通过.去访问对应的元素。
2.list的实现
2.1 基本结构
节点:
由头尾指针,一个数据构成。
链表:
2.2 基本接口
list() {
_head = new Node();
_head->_next = _head;//_head是Node类型的变量,我们此处访问他的成员变量
_head->_prev = _head;//如果Node(也就是ListNode<T>)是一个class
} //成员变量默认就会变成私有,会被拒绝访问
复习如何push_back:
先找到当前的tail , 记录此节点的prev , 然后改变tail的next的指针指向,指向新加入的元素。
void push_back(const T& x) {
Node* tmp = new Node(x);
Node* pretail = _head->_prev;
pretail->_next = tmp;
tmp->_next = _head;
tmp->_prev = pretail;
_head->_prev = tmp;
为什么struct而不是class:
1. 模版不会被编译。只有在实例化以后才会编译模版类
2. ListNode没有构造函数
注意:new Node()和new Node是不一样的,后者不会调用构造函数,只是开空间
3.访问权限问题
解决如下:
template<typename T>
struct ListNode {
ListNode<T>* _next;
ListNode<T>* _prev;
T data;
ListNode(const T& x = T())
:_next(nullptr),//一个新创建的节点的next和prev不需要指向任何地方,赋给nullptr即可
_prev(nullptr),
_data(x)
{}
};
或者class加上修饰限定符public也可以:
关于哨兵位的初始化:不能用0,因为不确定T到底是int还是string或者vector等。。。。
建议使用匿名构造:
或者全缺省( 反正我们在ListNode<T>里面是加了参数T()的 )
3.迭代器(本篇重点)
3.1 迭代器的构建
之前都是使用的原生指针T*作为iterator
因为其物理空间都是连续的,所以可以直接用指针。
而我们刚才由源码中观察得到,链表的指针是封装过的自定义类型。
实现iterator:
先实现框架:
template<typename T>
class ListIterator {
typedef ListNode<T> Node;
typedef ListIterator<T> _self;//这里是模仿库中的写法,将自己的名字缩短一点
Node* _pnode;
public:
ListIterator(Node* pnode) //之后在list中就可以通过iterator it(li1.begin())等构造
:_pnode(pnode)// iterator it = li1.begin()也可以,这是隐式类型转换
{}
operator++
operator--
};
再来实现++(要在参数处加int才是后置加加):
_self& operator++() {
_pnode = _pnode->_next;
return *this;
}
小练习:
_self operator++() {
return _self(_pnode->_next);//测试一下能不能跑,并阐述理由
}
同理实现--和解引用:
_self& operator--() {
_pnode = _pnode->_prev;
return *this;
}
T& operator*() {
return _pnode->_data;
}
使用迭代器访问各种元素时,经常用到!=判断是否走到了末尾:
bool operator!=(const _self& it) {
return this->_pnode != it._pnode;
}
除了前置++--,还有后置的:
3.2 联系list
再回到list中:
iterator begin() {
return iterator(_head->_next);
}
iterator end() {
return iterator(_head);
}
同时,在调用测试时:
因此还需要把 ListIterator中的几个函数变为公有。
(!=作为重载的运算符,因为需要访问it的_node所以也是不能访问的)
并且,由于for循环就是自动去找iteraotr和begin()与end()三个名字,我们也能使用循环for
我们不能直接改变内置类型的行为,但是可以通过封装的方式来将该内置类型封装成想要的样子。
理解清楚这张图:
this是一个指向self(也就是ListIterator<T>)的指针 ,return *this就相当于把这个iterator传回去了
3.3 ListIterator的完善
3.3.1 不需要写析构函数
3.3.2 拷贝构造和赋值运算符
3.3.3 operator-> (双箭头变单箭头)
先实现一个箭头访问:
(注意,_node的类型是Node* , 箭头优先级也是高于取地址的)
假设我们的T是一个控制坐标的自定义类型Pos
我们再对自己的链表进行测试:
it解引用之后是一个Pos,但是Pos没法直接被留提取<<给输出,因此报错。
我们当然可以“投降”,一个一个的访问元素:
但是我们更应该直接通过iterator去访问pos的成员变量:
(日常的迭代器用operator->的机会不多,之后在学习图(map)的时候会比较多)
特殊现象:
关于为什么begin()的返回值可以++,也就是
++li1.begin();
4.const迭代器
不能直接在list<int> :: iterator 前面加const ,
const list<int>::iterator it2 = li1.begin();//错误样例
因此我们可以实现两个类,一个是刚刚的ListIterator ,另一个是我们再实现一个ListConstIterator
改一改名字,修改一下两个核心模块的函数返回值即可。
template<typename T>
class ListConstIterator {
typedef ListNode<T> Node;//与class list中的命名保持一致性
typedef ListConstIterator<T> _self;
Node* _pnode;
public:
ListConstIterator(Node* pnode)
:_pnode(pnode)
{}
//_self operator++() {
// return _self(_pnode->_next);//测试一下能不能跑
// }
_self& operator++() {
_pnode = _pnode->_next;
return *this;
}
_self operator++(int) {
_pnode = _pnode->_next;
return _self(_pnode->_prev);
}
_self& operator--() {
_pnode = _pnode->_prev;
return *this;
}
_self operator--(int) {
_pnode = _pnode->_prev;
return _self(_pnode->_next);
}
const T& operator*() {
return _pnode->_data;
}
const T* operator->() {
return &_pnode->_data;
}
bool operator!=(const _self& it) {
return this->_pnode != it._pnode;
}
};
这样我们就能达到目的了。
别忘了再去写const对应的begin和end
注意此处的lt也必须是const list<int> lt ;
读者朋友们是否会觉得这样的实现有点过于冗余呢?
因为ListIterator<T>和ListConstIterator<T>两者非常相像,只有少数接口不一样
能否不定义一个新的类呢:
class ListIterator是满足了要求,但是class list中依然实例化的是T, 若调用:
那么初始化const_iterator的就是一个T而非const T,会报错。
解决方案:
直接将引用和指针传入,一个不使用const , 一个使用const修饰
iterator:
对照着来看:
5. list中的insert和erase
首先看insert :
按照合理的顺序更改各个指针的指向即可
再看返回值:
指向用户插入的第一个元素:
iterator insert(iterator pos, const T& x) {
Node* tmp = new Node(x);
tmp->_next = pos._pnode;
tmp->_prev = pos._pnode->_prev;
pos._pnode->_prev->_next = tmp;
pos._pnode->_prev = tmp;
return iterator(tmp);
}
(返回临时变量时就不要传引用返回了,返回一个tmp的拷贝更加合适)
类似于vector , erase返回的也是被删除的下一个位置的迭代器
同时还要注意断言,不要把哨兵位给删掉了
iterator erase(iterator pos) {
assert(pos != end());//别把哨兵位给删了
Node* cur = pos._pnode;
Node* prev = pos._pnode->_prev;
Node* next = pos._pnode->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}