list模拟实现
结点类的模拟实现
list的底层实现其实是一个链表,而且还是一个带头循环双向链表:
我们需要先实现一个结点类,结点类中包括数据域,前后结点的地址:
namespace gtt
{
template<class T>
struct list_node
{
T _data;
list_node<T>* _prev;
list_node<T>* _next;
};
}
构造函数
结点类的构造函数就是根据所给数据来构造一个节点,节点的数据域存储数据,前后指针指向nullptr即可,若构造结点时未传入数据,则默认以list容器所存储类型的默认构造函数所构造出来的值为传入数据:
//构造函数
list_node(const T& val = T())
:data(val)
,prev(nullptr)
,next(nullptr)
{}
迭代器类的模拟实现
迭代器类存在的意义
我们在学习vector与string过程中,并没有去创建一个迭代器类,而list就需要去创建一个迭代器类,这是为什么呢?
在vector与string学习中,我们可以发现,在物理内存中它俩都是一个连续的结果,此时的指针就相当于是一个原生指针,我们对指针进行自增,自减和解引用操作,就直接对相应位置是数据进行操作:
而对于我们对list来说,它是一个链表结构,在物理内存中并不是连续的,我们并不能只通过指针的自增,自减和解引用来对相应的数据进行操作:
既然list的指针不满足原生指针的要求,我们此时就可以对list的指针直接进行封装,通过运算符重载,来实现自增,自减,以及解引用等功能,让list可以和vector,string一样,看起来像原生指针一样对进行自增,自减和解引用就是在对数据进行操作。
我们通过这种封装的方式,可以使使用者不必关心容器底层实现,用起来的就是简单的方式访问数据。
迭代器类的模板参数
迭代器类模板参数一共有三个:
template<class T, class Ref, class Ptr>
我们为什么要传递三个参数呢?那是因为我们在实现list的时候,typedef了两个迭代器类型:
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
我们就可以发现,Ref和Ptr分别代表引用类型与指针类型,当我们调用const类型迭代器时,编译器就会实例化一个const类型迭代器出来,调用普通类型迭代器时,编译器就会实例化一个普通类型的迭代器。
构造函数
迭代器其实就是对指针进行了封装,他的成员变量就只有一个,就是节点指针,他的构造函数就是直接根据所给指针构造一个迭代器函数即可:
//构造函数
__list_iterator(Node* node)
:_node(node)
{}
!=运算符重载
使用!=运算符,本质就是看两个迭代器中的指针是否指向不同位置:
//不等于
bool operator!=(const iterator& it)const
{
return _node != it._node;
}
==运算符重载
使用 ==运算符与使用 !=运算符意思相反,本质就是看两个迭代器中的指针是否指向相同位置:
//等于
bool operator==(const iterator& it)const
{
return _node == it._node;
}
*运算符重载
解引用操作就是找到该节点的数据,我们直接返回当前节点指针所指向的数据即可,要注意的是这儿得使用引用返回,有可能需要对数据进行修改:
//解引用
Ref& operator*()
{
return _node->_data;
}
->运算符的重载
->操作主要是针对于某些自定义类型时,比如日期类,我们需要访问它的成员变量时,单独打印出年月日,就需要用到->操作符了,我们直接返回当前指针所指向的数据即可:
//->
Ptr operator->()
{
return &(operator*());
//return &(_node->_data);
}
其实他本质上是应该是两箭头,以日期类为例:第一个箭头是pos ->去调用重载的operator->返回Date* 的指针,第二个箭头是Date* 的指针去访问对象当中的成员变量_year。
++运算符的重载
++运算符分为前置++和后置++,
- 前置++,string和vector中的前置++就是返回自增后的数据,我们为了让list也能表现出此特性,前置++就是返回后一个指针指向的结点:
//前置++
iterator& operator++()
{
_node = _node->_next;
return *this;
}
- 后置++,也就是先记录当前结点位置,进行自增以后,返回自增前指针指向的结点:
//后置++
iterator operator++(int)
{
iterator tmp(*this);
_node = _node->_next;
return tmp;
}
- -运算符的重载
同样,- -运算符分为前置- -和后置- -,他的原理与前置++类似:
//前置--
iterator& operator--()
{
_node = _node->_prev;//自减后指针指向下一个结点
return *this;//返回自减后的结点
}
//后置--
iterator operator--(int)
{
iterator tmp(*this);//记录当前结点位置
_node = _node->_prev;//自减后指针指向下一个结点
return tmp;//返回自减前结点
}
list模拟实现
构造函数
因为list是一个带头循环双向链表,我们只需要申请一个头结点,然后头尾都指向头结点即可:
template<class T>
class list
{
typedef list_node<T> Node;
public:
list()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
private:
Node* _head;
};
拷贝构造函数
1.在这儿我们可以先构造一个容器出来,然后将在进行交换即可,这种就属于现代写法:
void emptyinit()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
template <class InputIterator>
list(InputIterator first, InputIterator last)
{
emptyinit();
while (first != last)
{
push_back(*first);
first++;
}
}
list(const list<T>& lt)
{
emptyinit();//创建一个头结点
list<T> tmp(lt.begin(), lt.end());//构造一个新容器
swap(tmp);//容器进行交换
}
2.创建一个头结点,将原节点挨个尾插进去:
list(const list<T>& lt)
{
emptyinit();
for (const auto& e : lt)
{
push_back(e);
}
}
赋值运算符重载函数
1.传统写法:先调用clear()函数对容器进行清理,然后将lt节点挨个尾插到容器中
//传统写法
list<int>& operator=(const list<int>& lt)
{
if (this != <)//判断是否自己给自己赋值
{
clear();//清理容器数据
for (const auto& e : lt)
{
push_back(e);//尾插
}
}
return *this;
}
2.现代写法:函数传值传参,然后将容器进行交换
//现代写法
list<int>& operator=(list<int> lt)
{
swap(lt);
return *this;
}
与迭代器相关的函数
begin与end
begin函数是返回第一个有效数据的迭代器,end函数返回最后一个有效数据下一个位置的迭代器。
list中第一个有效数据的迭代器就是头结点的下一个结点构造出来的迭代器,最后一个有效数据下一个位置的迭代器就是头结点构造出来的迭代器:
//begin()
iterator begin()
{
return iterator(_head->_next);
}
//end()
iterator end()
{
return iterator(_head);
}
我们还需要重载一对const对象的begin()函数和end()函数:
//const begin()
const_iterator begin()const
{
return const_iterator(_head->_next);
}
//const end()
const_iterator end()const
{
return const_iterator(_head);
}
访问容器相关的函数
front和back
front和back就是获取第一个有限数据和最后一个有效数据,我们只需要返回第一个有效数据和最后一个有效数据即可:
//front()
T& front()
{
return *begin();
}
//back()
T& back()
{
return *(end()--);
}
我们还需要重载一对const对象front()和back()函数:
//const front()
const T& front()const
{
return *begin();
}
//const back()
const T& back()const
{
return *(end()--);
}
插入与删除函数
insert与erase
insert:所给迭代器之前插入一个新结点。
//insert
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;//记录pos位置指针
Node* prev = cur->_prev;//记录pos前一个位置指针
Node* newnode = new Node(x);//创建插入节点
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
erase:删除所给迭代器位置的结点
//erase
iterator erase(iterator pos)
{
assert(pos._node);//判断pos结点合法性
Node* cur = pos._node;//记录pos结点指针
Node* prev = cur->_prev;//记录pos前一个结点位置指针
Node* next = cur->_next;//记录pos后一个结点位置指针
prev->_next = next;
next->_prev = prev;
delete cur;//释放掉pos结点
return iterator(next);
}
我们需要注意的是erase以后pos是会失效的,所以这儿返回的是erase后pos下一个位置的迭代器。
push_back与pop_back
push_back与pop_back就是尾插跟尾删,我们只需复用insert与erase函数即可:
//尾插
void push_back(const T& x)
{
insert(end(), x);
}
//尾删
void pop_back()
{
erase(end()--);
}
front_back与front_back
front_back与front_back就是头插跟头删,我们也只需复用insert与erase函数即可:
//头插
void push_front(const T& x)
{
insert(begin(), x);
}
//头删
void front_back()
{
erase(begin());
}
其他函数
clear
clear用于清空容器,我们只需遍历一遍,删除结点,最后只剩头结点即可:
//clear
void clear()
{
auto it = lt.begin();
while (it != end())
{
it = erase(it);
}
}
empty
判断list是否为空,我们只需要判断begin()函数与end()函数迭代器的位置是否一样,此时就说明list中只有一个头结点。
//empty
bool empty()
{
return (begin() == end());
}
swap
swap用于交换两个容器,我们只需将两个容器的头结点进行交换即可:
//swap
void swap(list<T>& lt)
{
::swap(_head, lt._head);
}