赞
踩
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类在类外直接访问节点。
前几篇博客中讨论的容器,比如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的构造函数之前,首先需要一个头节点构造的函数,用于对头节点进行创建。
//头节点创建
void CreateHead()
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
}
list中,基本的构造函数就是只创建一个头节点。
//基本构造
list()
{
CreateHead();
}
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); }
比较建议写第二种,简单易懂。
重载赋值运算符与拷贝构造一样,也有两种方法:
第一种是将赋值对象的每一个元素进行尾插;
第二种是在参数中赋值对象不加引用,由于此时传过来的是拷贝构造的临时对象,与当前对象进行交换即可。
//重载=运算符
//赋值的话,没有定义过的是调拷贝构造,定义过的是赋值重载
list<T>& operator=(const list<T> x)
{
//第一种方法:就是将x的每个元素尾插
//第二种方法:定义一个临时对象,将头节点交换
swap(tmp);
return *this;
}
注意:赋值时,如果被赋值的对象是未定义的,此时调用的是拷贝构造;定义过的调用的是赋值重载。
//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;
}
//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函数
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函数
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;
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。