这一讲,我准备讲一下list的模拟实现。
list模拟实现首先需要有一个实体类list,我们有C语言数据结构的经验,当时实现链表的时候,我们要有头节点,有next指针,prev指针,val值,既然C语言可以这么写,C++也可以这么实现,但是我这里采用的方法是将节点单独封装一个类,list的成员变量就是这个类的实例化对象,对应到具体的逻辑就是我们的头节点也就是哨兵位,此外,还需要加一个size成员变量指定节点个数,不然不太方便。
cpp
template<class T>
struct node
{
node(const T& val = T())
:_prev(nullptr)
,_next(nullptr)
,_val(val)
{
}
node* _prev;
node* _next;
T _val;
};
接下来是关于list类的初始化方法,初始化的核心是不管哪种初始化,list一定要维护一个哨兵位,所以即使是默认初始化也要初始化一个哨兵位节点。
cpp
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
list()
{
empty_init();
}
单独封装一个empty_init()方法的原因一会解释。
list的构造方法还有三种,分别是n值构造、迭代器构造、拷贝构造,这三种都需要遍历链表,而遍历链表所需要的核心就是迭代器。
链表的迭代器不同string和vector,由于链表底层并非连续存储,所以不能像以往一样++--,需要对操作符进行重载,此外根据上一讲介绍的双向迭代器概念,链表的迭代器是不能+-的。
关于迭代器,由于你要进行操作符重载,所以我选择封装一个迭代器类,从底层的角度思考,你拿到任意一个类型的迭代器一般都是要访问迭代器所指向的空间的成员,所以我们其实应该拿到迭代器所指向节点的值,因此这也是一个模板类,而且,我们要对迭代器的解引用操作符和->操作符进行重载,重载即是分别拿到对应的值以及对应值的地址,除此之外,在遍历过程中我们需要对迭代器进行比较,因此需要重载==!=两个操作符。
大体的思路有了,还有一个棘手的问题,我们知道迭代器是有很多种的,当前阶段的学习还不足以让我们手搓反向迭代器,因此正向迭代器也是有const和非const之分的,先前实现的不论是哪种都还需要一个类对另一种进行封装,这是一个解决方案,但其实两个类除了类型不一样,里边的逻辑几乎是没什么区别的,搞成两个类反而有点冗余。
针对这种方式C++标准委员会采用了增加模板参数的方法。
增加的模板参数一个传T的引用,一个传T的地址,而const类型的迭代器则是传const T&和const T*。因为只有这两个地方二者是不同的。
那为什么这么麻烦不直接在迭代器类型前加上const呢?因为二者区别还是蛮大的,如果你直接加上const,相当于这个迭代器已然是常量,你无法修改它的值,对应的,你压根就不能拿它遍历,显然我们要达到的并不是这个效果,我们只是不想让他修改对应空间里存放的数据罢了。
cpp
template<class T,class Ref,class Ptr>
struct list_iterator
{
typedef node<T> Node;
typedef list_iterator<T, Ref, Ptr> Self;
Node* _node;
list_iterator(Node* node)
:_node(node)
{
}
Ref operator*()
{
return _node->_val;
}
Ptr operator->()
{
return &_node->_val;
}
Self& operator++()
{
_node = _node->_next;
return *this;
//return list_iterator(_node->_next);
}
Self& operator--()
{
_node = _node->_prev;
return *this;
//return list_iterator(_node->_prev);
}
Self operator++(int)
{
Node* ret = _node;
_node = _node->_next;
return list_iterator(ret);
}
Self operator--(int)
{
Node* ret = _node;
_node = _node->_prev;
return list_iterator(ret);
}
bool operator==(const Self& it)
{
return _node == it._node;
}
bool operator!=(const Self& it)
{
return !(_node == it._node);
}
};
在讲完迭代器后就要实现接下来的几个构造了,实现剩下的构造函数,需要有插入的方法,list插入分为insert,尾插,头插,其实尾插和头插也就是insert的一种特殊情况。
再次之前,由于一般来讲,类里都会提供begin()/end()的方法,所以这里也需要提供便于尾插头插、尾删头删,这里begin()指向的是首元节点end()则是哨兵位,因为插入一般是在某个位置之前插入,如果尾插end()指定为最后一个元素的位置就会出现错误,因此这么设计。
cpp
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator begin()const
{
return const_iterator(_head->_next);
}
const_iterator end()const
{
return const_iterator(_head);
}
cpp
typedef node<T> Node;
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
不难发现,const和非const的两个迭代器构造方法都不一样,那我们这么写在底层和两个类有区别吗?其实是没有的,因为模板类本身就是编译器进行匹配生成具体的操作,相当于我们把自己的任务交给编译器做了,所以底层这两个还是两个类。
cpp
iterator insert(iterator pos, const T& val)
{
//pos前插入
Node* newnode = new Node;
newnode->_val = val;
Node* pos_node = pos._node;
Node* prev = pos_node->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos_node;
pos_node->_prev = newnode;
_size++;
return --pos;
}
iterator erase(iterator pos)
{
assert(size() > 0);
//返回未删除时的下一个
iterator ret = ++pos;
--pos;
Node* erase_node = pos._node;
Node* prev = erase_node->_prev;
Node* next = erase_node->_next;
prev->_next = next;
next->_prev = prev;
delete erase_node;
erase_node = nullptr;
_size--;
return ret;
}
void push_back(const T& val)
{
insert(end(), val);
}
void push_front(const T& val)
{
insert(begin(), val);
}
void pop_back()
{
iterator tmp = --end();
erase(tmp);
}
void pop_front()
{
erase(begin());
}
以上是插入删除的代码,这里还要讨论关于迭代器失效的问题,相比string和vector,list的insert并没有迭代器失效的问题,因为内存本就不是连续存储,pos永远都指向原先那个节点,但是erase就会触发迭代器失效的问题,因为在erase中已经将节点释放了,pos就是"野指针"。
紧接着终于可以提到拷贝构造和其他两种构造了,这也就可以填上之前埋的关于empty_init()方法的伏笔,如果你的list中连哨兵位都没有,你迭代器又指向谁?所以empty_init()就是用来创建哨兵位的。
cpp
list(size_t n, const T& val = T())
{
empty_init();
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
list(iterator first, iterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
first++;
}
}
cpp
list(const list<T>& lt)
{
empty_init();
for (auto e : lt)
{
push_back(e);
}
}
接着实现赋值运算符重载函数和析构函数。
cpp
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
cpp
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
cpp
list<T>& operator=(list<T>& lt)
{
swap(lt);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
_size = 0;
}
和以往不同的是,析构需要逐个节点释放,所以我这里实现并借助了clear()方法,在析构中手动释放哨兵位,至于赋值,还是老方法,实现一个自己的交换函数并交给编译器自动调用相应析构。