list的底层是带头双向循环链表,其基本功能的实现需要三个类模板(节点类,迭代器类,和list类)共同完成
template<class T>
struct ListNode
{
// 默认的构造函数
ListNode(const T& x = T())
:_val(x)
, _prev(nullptr)
, _next(nullptr)
{}
T _val; // 存储的数据
ListNode<T>* _prev; // 指向前一个节点
ListNode<T>* _next; // 指向后一个节点
};
list类的迭代器不再是像string或者vector类的原生指针了,因为list的各个节点在物理空间上不是连续的,不能把节点的指针直接++或者- -得到前后位置的迭代器;又因为它的数据保存在节点的数据域中,不能把节点的指针解引用直接得到里面数据。所以我们要封装节点的指针形成一个迭代器类,重载这个类的operator* 和operator++等运算符,去模拟像指针一样的行为。
template<class T, class Ref, class Ptr>
struct ListIterator
{
// 类里还会用到这些模板类,为了简洁,我们这里给类名重定义
typedef ListNode<T> ListNode;
typedef ListIterator<T, Ref, Ptr> self;
ListIterator(ListNode* pnode)
:_node(pnode) // 初始化列表进行初始化
{}
ListNode* _node;
}
因为list类的底层是带头双向循环链表,所以我们只要知道头结点(即数据域无效,指针域有效的节点)就可以通过它的_next得到第一个节点,通过它的_prev得到最后一个节点,对实现链表的遍历和插入操作很方便。我们创建一个list对象,其成员变量就是一个头结点的指针,后面对链表的操作都通过这个头结点来完成。
template<class T>
class list
{
public:
typedef ListNode<T> ListNode;
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
private:
ListNode* _head;// 保存一个list类对象的头结点
}
为什么迭代器模板和list类模板的成员变量都是指向节点的指针,不能直接搞成节点吗?
函数原型
iterator begin();
const_iterator begin() const;
作用
返回该list类对象第一个节点的迭代器
// 非const对象就返回非const迭代器
iterator begin()
{
//用第一个节点(即头结点的下一个节点)构造一个迭代器对象返回
return iterator(_head->_next);
}
// const对象返回const迭代器
const_iterator begin() const
{
return const_iterator(_head->_next);
}
函数原型
iterator end();
const_iterator end() const;
作用
返回list类对象的头结点的迭代器
iterator end()
{
return iterator(_head);
}
const_iterator end() const
{
return const_iterator(_head);
}
补充:being/rbegin 和 end/rend的对比
函数原型
iterator insert (iterator position, const value_type& val);
作用
在pos这个迭代器位置插入一个值为val的节点,并返回这个节点的迭代器
iterator insert(iterator pos, const T& x)
{
// 创建一个新的节点
ListNode* pnewnode = new ListNode(x);
// 获取该迭代器位置的节点的地址
ListNode* cur = pos._node;
// 获取前一个节点的地址
ListNode* prev = cur->_prev;
// 插入新节点
prev->_next = pnewnode;
pnewnode->_prev = prev;
pnewnode->_next = cur;
cur->_prev = pnewnode;
// 构造一个该新节点的迭代器,并拷贝构造返回
return iterator(pnewnode);
}
void push_back(const T& x)
{
// 写法一(复用insert实现尾插)
insert(end(), x);
// 写法二(常规写法)
ListNode* pnewnode = new ListNode(x);
ListNode* tail = _head->_prev;
tail->_next = pnewnode;
pnewnode->_prev = tail;
_head->_prev = pnewnode;
pnewnode->_next = _head;
}
函数原型
iterator erase (iterator position);
作用
删除指定位置节点,并返回下一个节点的迭代器
iterator erase(iterator pos)
{
// 通过迭代器获取节点的地址
ListNode* cur = pos._node;
// 记录要删除节点的前后节点
ListNode* prev = cur->_prev;
ListNode* next = cur->_next;
// 删除节点
delete cur;
// 连接原来的前后节点
prev->_next = next;
next->_prev = prev;
// 构造下一个节点的迭代器对象并返回
return iterator(next);
}
void pop_back()
{
// end是获取到头结点,- -节获取到最后一个节点
erase(--end());
}
list()
作用:构造空的list,即只创建一个不存有效数据的头结点
list类的成员变量只有一个指向头结点的指针,创建一个list类对象,就是让它的成员变量_head指向一块我们手动用new开辟的节点类的空间。由于构造函数有好几种形式,都要让_head指向一块空间,我们让这个步骤在 GreatHeadNode()这个函数里实现了。
list()
{
CreatHeadNode();
}
list (InputIterator first, InputIterator last)
作用:这是一个函数模板,用其他list类的迭代器[first, last)区间(左闭右开)中的元素构造list。
template <class InputIterator>
list(InputIterator first, InputIterator last)
{
// 先开头结点的空间
CreatHeadNode();
// 在头结点后尾插元素
while (first != last)
{
push_back(*first);// 这里的解引用操作的实现在
++first;
}
}
// clear也一个成员函数,清理list对象的有效节点
void clear()
{
iterator it = begin();
// 遍历并删除每一个有效节点
while (it != end())
{
delete (it++)._node;
}
// 清理完所有有效节点后,更新头结点
_head->_prev = _head;
_head->_next = _head;
}
// 析构函数,复用clear
~list()
{
clear();
// 头结点释放
delete _head;
// 让list对象的_head指向nullptr
_head = nullptr;
}
list(const list& lt)
:_head(nullptr)
{
// 先开头结点空间
CreatHeadNode();
// 构造一个临时对象
list<T> tmp(lt.begin(), lt.end());
// 利用std的swap交换头结点
::swap(_head, tmp._head);
}
list<T>& operator=(list<T> lt)
{
::swap(_head, lt._head);
return *this;
}
这里我们再来看看迭代器类的基本框架
template<class T, class Ref, class Ptr>
struct ListIterator
{
// 类里还会用到这些模板类,为了简洁,我们这里给类名重定义
typedef ListNode<T> ListNode;
typedef ListIterator<T, Ref, Ptr> self;
ListNode* _node;
}
迭代器类的成员变量只有一个就是指向节点地址的指针变量_node,构造一个迭代器对象就传节点的地址,让_node指向节点的地址。
ListIterator(ListNode* pnode)
:_node(pnode) // 初始化列表进行初始化
{}
ListIterator(const self& it)
:_node(it._node) //初始化列表初始化
{}
因为物理空间上的不连续,迭代器就不是原生指针,不能拿到节点的地址直接进行解引用,自增,自减等操作。为了实现前面的这些功能,我们把节点的指针封装起来,就是现在的迭代器类。
在这之前我们再来看看节点类的框架
template<class T>
struct ListNode
{
// 默认的构造函数
ListNode(const T& x = T())
:_val(x)
, _prev(nullptr)
, _next(nullptr)
{}
T _val; // 存储的数据
ListNode<T>* _prev; // 指向前一个节点
ListNode<T>* _next; // 指向后一个节点
};
返回节点中值的引用
Ref operator*()
{
// 直接返回节点的数据
return _node->_val;
}
就是返回节点中值的指针
//可读,可不可以写取决于迭代器类型(决定Ptr是 const T*还是 T*)
Ptr operator->()
{
return &(operator*());
}
// 前置++(让迭代器对象的成员变量指向下一个节点,并返回它自己)
self& operator++()
{
_node = _node->_next;
return *this;
}
// 后置++(先构造一个它的迭代器,它自己指向下一个节点)
self operator++(int)
{
self tmp(_node);
_node = _node->_next;
return tmp;
}
// 前置- -
self& operator--()
{
_node = _node->_prev;
return *this;
}
// 后置- -
self operator--(int)
{
self tmp(_node);
_node = _node->_prev;
return tmp;
}
区分这两个对象
当他们都指向同一个节点时,在物理内存上他们都存这个节点的地址,在物理上他们都是一样的。但是它们的类型不一样,那么它们的意义也就不一样,因为类型决定了对空间的使用权。
比如:
*pnode是一个指针的解引用,取到的值是这个节点本身。
*it是去调用这个迭代器的operator *(),返回的值是这个节点中值的引用。
优点:
缺点:
优点:
缺点: