1、基础框架
从list源代码中我们可以看到list底层为带哨兵位双向循环链表,从设计者的角度来看,我们并不知道用户在链表中存储的数据类型,所以在这里要使用类模板。再者,每一个节点都包含了前向、后向指针和存储的数据值,所以在这里也可以将每一个节点封装成一个类,结点中的数据必须要在链表中能被访问,所以在设计结点的类时,类中的成员属性应该能被外部访问,即节点类内部应当使用相应的访问限定符进行修饰,也可以直接将class换成struct(struct默认访问限定符为public)
在书写代码之前我们应该理清两个类之间的关系:
- list:list为链表对象,成员属性为_head,类型为: 节点*。
- _head:指向哨兵位头节点的指针,节点内部存放的数据类型未知。初始化时_prev指向_head,_next指向_head。
那么代码实现就可以写成:
#pragma once
#include <iostream>
namespace ltq
{
template<class T>
struct list_node
{
typedef list_node<T> Node;//节点模板实例化
Node* _next;
Node* _prev;
T _val;
list_node()
:_next(nullptr)//新节点初始化成空指针
,_prev(nullptr)
,_val(T())
{}
};
template<class T>
class list
{
typedef list_node<T> Node;//节点模板实例化
public:
list()
:_head(new Node)//创建一个头节点
{
_head->_prev = _head;
_head->_next = _head;
}
private:
Node* _head;
};
}
2、push_back实现
双向带头循环链表的尾部插入是比较简单的,首先需要找到目前链表的尾节点,再创建新的节点并按要插入的数据要求对新节点进行初始化。初始化结束之后进行相应的链接就可以了。
void push_back(const T& val)
{
Node* tail = _head->_prev; //找到尾节点
Node* newNode = new Node(val);//调用构造函数创建节点对象
//进行链接
newNode->_prev = tail;
tail->_next = newNode;
newNode->_next = _head;
_head->_prev = newNode;
}
这里需要对list_node的构造函数进行修改,因为再创建节点的过程中,我们需要给定存储数据的具体数值,所以,在前面的list_node构造函数中给上缺省值。
list_node(const T& val = T())//给缺省值
:_next(nullptr)
,_prev(nullptr)
,_val(val)//缺省值或者参数进行初始化数据
{}
通过节点之间的连接,链表的逻辑结构就变成了下面的形式:
数据能否正常插入呢?运行代码没有出现错误信息,为了能够方便的验证,下面就先实现链表的迭代器。
3、迭代器实现
链表的迭代器要向string、vector一样的连续结构的迭代器一样去访问链表中的数据是值得思考的。连续的数据结构直接可以通过指针来实现迭代器,因为指针解引用就可以顺利的拿到该位置的数据,指针的加减操作就能轻松的实现目标位置的移动。然而,对于链表这种物理结构不连续的数据结构来说想达到像顺序结构一样的功能,显然就不能仅使用指针来进行实现。
ps:假设依然采用指针直接实现迭代器,那么对指针解引用是不是我们想要的节点内部的数据呢?显然,此时的解引用只是这个节点本身。并且此时倘若指针做加减操作,具体指向哪里,我们也不得而知。
综上,我们要实现和顺序结构功能相同的链表迭代器,就必须对迭代器进行封装,在迭代器内部使用操作符重载来实现预期的功能。这里的迭代器仍然是指向节点的指针,我们需要重载解引用操作符、以及其他操作符以实现正常功能。
显然,这里我们需要使用类模板来实现迭代器的功能描述。由于在list内部要使用迭代器的成员属性,这里直接就将其定义为struct类方便后续的操作。
template<class T>
struct __list_iterator
{
typedef list_node<T> Node;
Node* _node;
};
参考源码中的实现,迭代器的begin()返回哨兵位的下一个节点的指针,end()返回的是哨兵位节点的指针 。那么代码就可以写成:
typedef __list_iterator<T> iterator;
iterator begin()
{
return _head->_next;
}
iterator end()
{
return _head;
}
在定义时我们知道迭代器就是节点的指针,那么迭代器的构造函数就可以写成如下的形式:
template<class T>
struct __list_iterator
{
typedef list_node<T> Node;
Node* _node;
__list_iterator(Node* node)
:_node(node)
{}
};
这里会有一个疑问,begin()中返回的是_head->_next,它的类型为Node*,而迭代器尾__list_node<T>,这里其实发生了隐式类型转换。也就是单参数的构造函数执行隐式类型转换。
3.1、迭代器运算符重载
如上所述,我们首先需要对解引用操作符进行重载,我们期望对节点的迭代器解引用之后得到节点内的数据。数据类型为T,且函数调用结束之后_val 不会消失,故直接返回T引用。
T& operator*()
{
return _node->_val;
}
前置++和后置++的重载。这里需要明确的是,为了符合规范,迭代器的加减操作之后返回的数据都是迭代器类型。
__list_iterator<T> operator++()
{
_node = _node->_next;
return *this;
}
//后置++
__list_iterator<T> operator++(int)
{
//调用拷贝构造 创建中间迭代器对象
__list_iterator<T> tmp(_node);
_node = _node->_next;
return tmp;
}
在目前的基础上再重载一下!=符号,就可以实现对基本的迭代器的使用。
bool operator!=(const __list_iterator<T> it)
{
return _node != it._node;
}
测试一下上面的功能是否正常:
3.2、const对象的迭代器
如果按照顺序结构迭代器的套路来写,就会产生一个错误,像下面这样:
//以前的套路
typedef const __list_iterator<T> const_iterator;
const_iterator begin()const
{
return _head->_next;
}
const_iterator end()const
{
return _head;
}
返回值将会是一个const修饰的迭代器,更简单的说就是一个const 修饰的节点指针。那么就有大麻烦了!这个指针将不能移动!!!前面的++ 操作符压根没有办法使用。这时我们需要回头来思考一下,const对象的迭代器不期望的是修改节点内部的值,但是可以遍历访问。所以我们只需要做的操作就是再返回值T&那里 加上const修饰,那么返回的值将不能被修改。
由于const链表对象和普通的链表对象只是在这部分有差异,所以为了减少代码的冗余度,这里需要增加类模板的参数以实现代码的表面上的复用(对编译器来说这是两个类!并不是复用)。
template<class T,class Ref>
typedef __list_iterator<T, const T&> const_iterator;
const_iterator begin()const
{
return _head->_next;
}
const_iterator end()const
{
return _head;
}
4、insert实现
链表的pos位置插入是很容易实现的,库中一般规定在pos位置的前面进行插入。并且返回新插入节点的迭代器。
iterator insert(iterator pos,const T& val)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newNode = new Node(val);
prev->_next = newNode;
newNode->_prev = prev;
newNode->_next = cur;
cur->_prev = newNode;
return newNode;
}
5、erase实现
参考库中的形式,函数返回删除节点之后的节点的迭代器。
iterator erase(iterator pos)
{
Node* cur = pos._node;
Node* next = cur->_next;
Node* prev = cur->_prev;
prev->_next = next;
next->_prev = prev;
delete cur;
return next;
}