list的模拟实现
在上一篇博客中讲解了list的用法,了解了list的基本使用。这篇博客将会详细介绍list的底层实现原理,更加深入的了解list。
list的节点类型
list的底层是一个双向链表,因此list的节点中包括一前一后两个指针,以及节点的值val。指针prev指向该节点的前一个节点,指针next指向该节点的后一个节点。
//定义节点,通过模板来定义
template <class T>
//用struct定义成公有的,再list类中可以直接访问
struct List_Node
{
T _val;
List_Node<T>* _prev;
List_Node<T>* _next;
//这里的节点也相当于一个类,因此需要写一个构造函数,针对那些开辟空间时调用的初始化
List_Node(const T val = T())//构造匿名对象进行赋值
: _prev(nullptr)
, _next(nullptr)
, _val(val)
{}
};
为了能够使节点类型能够满足各种类型,因此节点的定义采用模板实现。并且将其定义为struct,方便list类在类外直接访问节点。
list的迭代器
前几篇博客中讨论的容器,比如string、vector等等的迭代器都是使用的原生指针,这是因为string、vector等容器底层空间是连续的,原生指针就具有迭代器的性质。
但是list每一个节点在底层空间中不连续,因此原生指针作为迭代器就不合适。因此list的迭代器实现就必须对原生指针进行封装,使得迭代器的使用形式与指针完全相同。
解引用
指针是可以解引用来获取指针对应的值,在list的迭代器中必须重载operator*。
在list重载operator也就是获取每个节点对应的值val,因此在list的迭代器中重载operator就是通过每一个节点的指针取到节点对应的值。
//重载*运算符
Ref operator*()
{
return _node->_val;
}
通过“->”访问成员
指针可以通过“->”访问其成员,这也是指针的特性之一。在list中想要完成“->”的重载,可以通过对operator*()进行取地址得到,使得重载后的“->”拿到节点的地址对节点成员进行访问。
//这里重载->是为了给自定义类型去使用,使得自定义类型能够通过指针来访问空间变量
Ptr operator->()
{
return &(operator*());
}
指针的前后移动
由于list的节点在底层空间中是不连续的,因此普通的++和–无法完成list中的前进和后退,必须在迭代器中对++和–进行重载。
++的操作通过节点中的next指针来完成;–的操作通过节点中的prev来完成。
//前置++和后置++的区别:
//1、前置++的参数中只有this指针,后置++的参数除了this指针,还有int进行占位
//2、前置++先修改再使用,所以返回值加引用;后置++先使用再修改,返回值没有引用。
//重载后置++运算符
self operator++(int)
{
_node = _node->_next;
return *this;
}
//重载前置++
self& operator++()
{
_node = _node->_next;
return *this;
}
//重载后置--运算符
self operator--(int)
{
_node = _node->_prev;
return *this;
}
//重载前置--
self& operator--()
{
_node = _node->_prev;
return *this;
}
相等的比较
由于迭代器还需要进行是否相等的比较,因此迭代器还必须对operator==和operator!=进行重载。
两个迭代器之间的比较就是比较两个节点的地址是否相同,那么在底层可以直接通过比较节点是否相等来完成。
//重载!=
bool operator!=(self& l)//比较的时候是同一类的对象,参数应该也是对象,不应该是节点
{
return _node != l._node;
}
//重载==
bool operator==(self& l)
{
return _node == l._node;
}
迭代器部分的完整代码如下:
//如果想实现const迭代器,如果重新写一个相似的类就会出现代码冗余,因此可以在模板参数这里做调整
//给定三个模板参数:T、Ref、Ptr
//当参数不同时,对象的类型也就不同
template <class T, class Ref, class Ptr>
struct List_Iterator
{
typedef List_Node<T> Node;
typedef Node* pNode;
typedef List_Iterator<T, Ref, Ptr> self;
public:
List_Iterator(pNode node = nullptr)//迭代器的构造函数就是根据某个节点创建的
: _node(node)
{}
//重载*运算符
Ref operator*()
{
return _node->_val;
}
//这里重载->是为了给自定义类型去使用,使得自定义类型能够通过指针来访问空间变量
Ptr operator->()
{
return &(operator*());
}
//针对const对象,这里不能在重载*这里加上const
//因为对象是const,所以返回的迭代器也应该是const迭代器
//const T& operator*() const
//{
// return _node->_val;
//}
//前置++和后置++的区别:
//1、前置++的参数中只有this指针,后置++的参数除了this指针,还有int进行占位
//2、前置++先修改再使用,所以返回值加引用;后置++先使用再修改,返回值没有引用。
//重载后置++运算符
self operator++(int)
{
_node = _node->_next;
return *this;
}
//重载前置++
self& operator++()
{
_node = _node->_next;
return *this;
}
//重载后置--运算符
self operator--(int)
{
_node = _node->_prev;
return *this;
}
//重载前置--
self& operator--()
{
_node = _node->_prev;
return *this;
}
//重载!=
bool operator!=(self& l)//比较的时候是同一类的对象,参数应该也是对象,不应该是节点
{
return _node != l._node;
}
//重载==
bool operator==(self& l)
{
return _node == l._node;
}
pNode _node;//迭代器中节点是基本的成员变量
};
这其中有一点需要注意:
迭代器一般分为非const迭代器和const迭代器,如果是原生指针的画,直接加const就可以完成。但是如果是封装的迭代器,要完成const迭代器,一种方法是写一份相似的迭代器封装,在某些位置加上const,但是这样做会出现代码冗余的情况;第二种方法就是在模板参数上做文章,在模板参数传三个参数,分别对应list的值、引用和指针,这样子我们如果想是用const类型的迭代器,直接传const类型的模板参数即可。
list的构造函数
头节点构造
在实现list的构造函数之前,首先需要一个头节点构造的函数,用于对头节点进行创建。
//头节点创建
void CreateHead()
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
}
基本构造函数
list中,基本的构造函数就是只创建一个头节点。
//基本构造
list()
{
CreateHead();
}
构造n个节点
n个节点的构造函数,在创建头节点之后,通过循环将节点尾插在头节点之后。
list(size_t n, const T& val = T())
{
CreateHead();
while (n--)
push_back(val);
}
这里val的缺省通过一个匿名对象来给定。
通过迭代器构造
由于迭代器根据容器的底层实现是不同的,因此通过迭代器构造list必须以模板来实现,这样才能实现对于各种迭代器的构造。这里的构造就是从前到后遍历迭代器,对每一个节点进行尾插。
//利用迭代器构造
template <class InputIterator>
list(InputIterator first, InputIterator last)
{
CreateHead();
while (first != last)
{
push_back(*first);
first++;
}
}
拷贝构造
对于拷贝构造,这里有两种方法:
第一种是定义一个头节点,将被拷贝对象中的每一个元素在新的对象中进行尾插。
第二种是通过构造一个临时对象,与新对象进行交换即可。
//拷贝构造函数
list(const list<T>& x)
{
//CreateHead();
//第一种方法:自己定义一个头节点,将x中的每个元素尾插
//const_iterator it = x.begin();
//while (it != x.end())
//{
// push_back(*it);
// it++;
//}
//第二种方法:定义一个临时对象,进行交换
list<T> tmp(x.cbegin(), x.cend());
swap(tmp);
}
比较建议写第二种,简单易懂。
重载operator=()
重载赋值运算符与拷贝构造一样,也有两种方法:
第一种是将赋值对象的每一个元素进行尾插;
第二种是在参数中赋值对象不加引用,由于此时传过来的是拷贝构造的临时对象,与当前对象进行交换即可。
//重载=运算符
//赋值的话,没有定义过的是调拷贝构造,定义过的是赋值重载
list<T>& operator=(const list<T> x)
{
//第一种方法:就是将x的每个元素尾插
//第二种方法:定义一个临时对象,将头节点交换
swap(tmp);
return *this;
}
注意:赋值时,如果被赋值的对象是未定义的,此时调用的是拷贝构造;定义过的调用的是赋值重载。
list的增删查改
尾插
//push_back函数
void push_back(const T& val)
{
pNode newnode = new Node(val);//这里可以认为是根据节点的类调用构造函数
pNode tail = _head->_prev;
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
}
头插
//push_front函数
void push_front(const T& val)
{
pNode newnode = new Node(val);
pNode first = _head->_next;
// _head newnode first
newnode->_prev = _head;
_head->_next = newnode;
newnode->_next = first;
first->_prev = newnode;
}
insert函数
//1、普通插入,在pos位置之前
void insert(iterator pos, const T& val)
{
assert(pos._node);//断言以下空指针
pNode newnode = new Node(val);
pNode cur = pos._node;
pNode prev = cur->_prev;
newnode->_prev = prev;
prev->_next = newnode;
newnode->_next = cur;
cur->_prev = newnode;
}
//2、填充n个值
void insert(pNode pos, size_t n, const T& val)
{
assert(pos);
while (n--)
{
insert(pos, val);//复用前面的insert
pos = pos->_prev;
}
}
尾删
//pop_back函数
void pop_back()
{
assert(_head != _head->_next);
pNode tail = _head->_prev;
pNode last = tail->_prev;
delete tail;
last->_next = _head;
_head->_prev = last;
}
头删
//pop_front函数
void pop_front()
{
assert(_head != _head->_next);
pNode first = _head->_next;
pNode newfirst = first->_next;
delete first;
_head->_next = newfirst;
newfirst->_prev = _head;
}
erase函数
//erase函数
pNode erase(pNode pos)
{
assert(_head != _head->_next);
pNode prev = pos->_prev;
pNode next = pos->_next;
delete pos;
prev->_next = next;
next->_prev = prev;
return next;
}
clear函数
//clear函数
void clear()
{
//如果只有一个头节点,不需要动
assert(_head->_next == _head);
pNode cur = _head->_next;
while (cur != _head)
{
pNode next = cur->_next;
pop_back();//复用尾删
cur = next;
}
}
析构函数
//析构函数
~list()
{
pNode cur = _head->_next;
while (cur != _head)
{
pNode next = cur->_next;
delete cur;
cur = next;
}
delete _head;
}