namespace mlf
{
//结点类
template<class T>
struct _list_node
{
//成员函数
_list_node(const T& val = T()); //构造函数
//成员变量
T _val; //数据域
_list_node<T>* _next; //后继指针
_list_node<T>* _prev; //前驱指针
};
//迭代器类
template<class T, class Ref, class Ptr>
struct _list_iterator
{
typedef _list_node<T> node;
typedef _list_iterator<T, Ref, Ptr> self;
_list_iterator(node* pnode); //构造函数
//各种运算符重载函数
self operator++();
self operator--();
self operator++(int);
self operator--(int);
bool operator==(const self& s) const;
bool operator!=(const self& s) const;
Ref operator*();
Ptr operator->();
//成员变量
node* _pnode; //一个指向结点的指针
};
//list类
template<class T>
class list
{
public:
typedef _list_node<T> node;
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
//默认成员函数
list();
list(const list<T>& lt);
list<T>& operator=(const list<T>& lt);
~list();
//迭代器相关函数
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
//访问容器相关函数
T& front();
T& back();
const T& front() const;
const T& back() const;
//插入、删除函数
void insert(iterator pos, const T& x);
iterator erase(iterator pos);
void push_back(const T& x);
void pop_back();
void push_front(const T& x);
void pop_front();
//其他函数
size_t size() const;
void clear();
bool empty() const;
void swap(list<T>& lt);
private:
node* _head; //指向链表头结点的指针
};
}
list的底层其实是一个带头双向循环链表,我们在实现list类之前,还需要先实现一个结点类。链表里面一个结点需要存储的数据有:该结点的数据,前一个结点的地址,后一个结点的地址。
因此结点类的成员变量为:
//成员变量
T _val; //数据域
_list_node<T>* _next; //后继指针
_list_node<T>* _prev; //前驱指针
对于结点类来说,我们只需要实现它的构造函数,析构函数则交给list类去完成即可,下面我们来看一下结点类的构造函数。
结点类的构造函数需要我们根据所给的数据去构造一个结点,将所给数据赋给我们的数据域,将结点的前驱指针和后继指针都指向nullptr,当一个指针不知道它的具体指向的时候我们就将它置成空指针。
//构造函数
_list_node(const T& val = T())
:_val(val)
, _prev(nullptr)
, _next(nullptr)
{}
注意:如果未给定数据,那么将会以list容器中所存储类型的默认构造函数所构造出来的值赋给我们的数据域。 其实在C++中内置类型也有它的默认构造函数,比如说int它默认构造函数所构造出来的值就是0。
我们之前模拟实现string与vector的时候都没说要实现迭代器类,怎么到了list这里我们就需要实现迭代器类了呢?
对于string和vector来说,他们的底层是一块连续的物理空间,我们可以通过迭代器来完成++、–、*等操作,就可以对相应的数据完成一系列的操作。这样的迭代器就称为是原生指针,像string与vector中的迭代器就是原生指针。
但是我们list的底层不是一块连续的物理空间,,我们不能通过迭代器来完成++、–、*等操作而直接对相应的数据完成一系列的操作。因此list的迭代器不是原生指针。
由于list结点的指针原生行为不满足迭代器定义,那么我们就需要对这个结点指针进行封装,重载结点指针的各个运算符,使得我们可以像vector和string那样去使用迭代器。
有了这样的方式,使用者可以不必关心容器底层结构到底是数组、链表、树形结构等等,封装隐藏了底层的细节,让我们可以用简单统一的方式去修改容器,这个也就是迭代器的真正价值。
我们迭代器类中的模板参数有如下三个
template<class T, class Ref, class Ptr>
这个时候可能大家会问了:为什么这里会有三个模板参数呢?
第一、为了防止我们的代码出现大量冗余
第二、为了更好的区分普通迭代器和const迭代器
第三、为了更方便的表示数据类型
我们知道迭代器分为普通迭代器和const迭代器,而我们在list类的模拟实现中分别typedef了普通迭代器和const迭代器。
typedef _list_iterartor<T, T&, T*> iterator;
typedef _list_iterartor<T, const T&, const T*> const_iterator;
因此我们就知道Ref和Ptr分别表示的是list中存储数据类型的引用和指针。
当我们使用普通迭代器的时候,编译器会根据模板实例化出一个普通的迭代器对象,当我们使用const迭代器对象时,编译器就会根据模板实例化出一个const迭代器对象。
list中的迭代器其实就是对结点指针进行了封装,我们迭代器类的成员变量只有一个,那就是结点指针。因此我们的构造函数就很简单了,我们只需要将所给的结点指针封装成一个迭代器对象就可以了。
//构造函数
_list_iterator(node* pnode)
:_pnode(pnode)
{}
我们使用解引用运算符,是想得到当前迭代器位置的数据内容,因此我们只需要返回当前结点指针指向的数据即可。
Ref operator*()
{
return _pnode->_val;
}
大家可能不太明白这里为什么还需要重载->运算符,其实在某些场景下我们是需要用到->运算符的,比如说:
当我们list容器中存储的数据类型是日期类时
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
public:
int _year;
int _month;
int _day;
};
void test_list4()
{
list<Date> lt;
lt.push_back(Date(2020,5,1));
lt.push_back(Date(1998,6,20));
lt.push_back(Date(2021,3,5));
list<Date>::iterator it = lt.begin();
while (it != lt.end())
{
cout << (*it)._year << " " << (*it)._month << " " << (*it)._day << endl;
++it;
}
cout << endl;
}
可以看到我们想打印list存储的日期类数据时,需要通过(*it)._year的方式才能够把我们想要的数据给打印出来,但是这样是不是太麻烦了呢?因此我们就需要重载一下我们的->运算符
对于我们->运算符的重载, 只需要返回结点指针指向的数据的地址即可。
Ptr operator->()
{
return &_pnode->_val;
}
因此我们上面情景的打印方式就可以变成这样
void test_list4()
{
list<Date> lt;
lt.push_back(Date(2020,5,1));
lt.push_back(Date(1998,6,20));
lt.push_back(Date(2021,3,5));
list<Date>::iterator it = lt.begin();
while (it != lt.end())
{
cout << it->_year << " " << it->_month << " " << it->_day << endl;
++it;
}
cout << endl;
}
大家看到这里,可能还是会有疑问:->运算符只是返回结点指针指向的数据的地址,但是这里怎么就直接打印出Date日期的年月日了呢?
其实这里本来是应该有两个->的,第一个箭头是it去调用operator->返回Date *的指针,第二个->是通过Date *的指针去访问日期类对象的成员变量_year。
但是因为这里在一个地方出现了两个->,程序的可读性不高,因此为了增加代码段可读性,编译器在这里做了优化,两个->变成了一个->
前置++运算符实现比较简单我们只需要让当前结点的指针指向后一个结点,然后再返回当前结点的迭代器即可。
//++it -> it.operator++(&it)
self& operator++()
{
_pnode = _pnode->_next;
return *this;
}
注意:这里的self是当前迭代器对象的类型
后置++运算符的实现我们需要先拷贝一下当前结点的迭代器,然后再让当前结点的指针指向后一个结点,因为后置++是返回++之前的值,因此我们需要返回一下刚才拷贝的迭代器。
//it++ -> it.operator++(&it,0)
self operator++(int)
{
self temp(*this);
_pnode = _pnode->_next;
return temp;
}
我们需要让当前结点的指针指向前一个结点,然后返回一下当前结点的迭代器即可
//--it -> it.operator--(&it)
self& operator--()
{
_pnode = _pnode->_prev;
return *this;
}
对于后置–运算符的实现我们需要先拷贝一下当前结点的迭代器,然后再让当前结点的指针指向前一个结点,因为后置–是返回–之前的值,因此我们需要返回一下刚才拷贝的迭代器。
//it-- -> it.operator--(&it,0)
self operator--(int)
{
self temp(*this);
_pnode = _pnode->_prev;
return temp;
}
==运算符的重载实现起来也比较简单,要想知道两个迭代器是否属于同一位置的迭代器,我们只需要判断一下两个迭代器中结点指针的指向是否相同即可。
bool operator==(const self& lt)
{
return _pnode == lt._pnode;
}
这里!=运算符的作用和上面正好想法,因此实现的时候只需要把==改成!=即可。
bool operator!=(const self& lt)
{
return _pnode != lt._pnode;
}
由于我们的list是带头双向循环链表,因此我们这里的构造函数需要申请一个头结点,然后再让前驱指针和后继指针都指向自己。
//构造函数
list()
{
//_head = new node(T());
_head = new node;//申请一个头结点
_head->_next = _head;
_head->_prev = _head;
}
对于拷贝构造我们需要手动申请一个头结点,然后再让前驱指针和后继指针都指向自己,最后通过遍历的方式将被拷贝对象的数据尾插到当前对象中。
//拷贝构造
//s1(s3)
list(const list<T>& lt)
{
_head = new node;
_head->_next = _head;
_head->_prev = _head;
for (const auto& e : lt)
{
push_back(e);
}
}
对于赋值运算符重载,这里有两种写法
写法一:
我们先调用clear函数,清空当前list容器里面的数据,然后再通过遍历的方式将lt容器里面的数据尾插到清空后的list容器中。
//赋值运算符重载
//实现方法一
//lt1 = lt3
list<T>& operator=(const list<T>& lt)
{
//先清空list里面的数据
clear();
//防止自己给自己赋值
if (this != <)
{
for (const auto& e : lt)
{
push_back(e);
}
}
return *this;
}
注: 这里的clear函数的实现在后面
写法二:
我们这里的lt并不是引用传参,而是传值传参,所以lt就是外面lt的拷贝构造,里面lt的修改并不会影响外面的lt,这是一个小细节。
其次我们只需要交换this与v里面的数据,最终再返回*this就能达到想要的结果了。
这里还是用string与vector里面点外卖的那个例子说一下:我们想吃东西,但是又不想自己做,那么我就点外卖让骑手**(相当于这里的lt)给我送过来(相当于这里的swap(lt)),早上妈妈出去的时候,叫我下楼的时候记得把垃圾给丢下去,可是我上午出去的时候忘记丢了,可是这个时候我又不想丢垃圾那我就让骑手帮我丢(我自己的这块空间不想要了,那我将里面的数据和lt里面的数据交换之后,等函数调用结束时,lt会自动调用它的析构函数,从而就帮助我们处理掉了这块空间),**不帮我丢的话就给差评(典型不当人行为hhhh)。
//实现方法二
//lt1 = lt3
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
注:swap函数的实现在后面
我们这里析构函数的实现比较简单,调用clear函数清空list容器中的数据,然后再释放头结点,最后将头结点置空即可。
//析构函数
~list()
{
clear();
delete _head;//释放头结点
_head = nullptr;//将头结点置空
}
对于迭代器相关的函数我们需要实现两个版本
版本一:只支持遍历访问list里面的数据内容,不能够去修改list里面的数据内容,并且我们如果不需要修改成员变量,我们最好在外面给this指针加上一个const,这样的话const对象可以调用它,非const对象也可以调用它
版本二:不仅支持遍历访问list里面的数据内容,还可以去修改list里面的数据内容。
在带头双向循环链表中,第一个有效数据位置的迭代器是用头结点的后一个结点的地址构造出来的迭代器,而最后一个有效数据位置的后一个位置的迭代器使用头结点的地址构造出来的迭代器。
begin函数的作用是返回容器中第一个有效数据位置的迭代器
//可读可写
//返回头结点的下一个结点的地址构造出来的迭代器
iterator begin()
{
return iterator(_head->_next);
}
//只读
//不修改最好加上const
//这样const对象可以调用,非const对象也可以调用
const_iterator begin()const
{
return const_iterator(_head->_next);
}
end函数的作用是返回容器中最后一个有效数据位置的下一个位置的迭代器
//可读可写
//返回头结点的地址构造出来的迭代器
iterator end()
{
return iterator(_head);
}
//只读
//不修改最好加上const
//这样const对象可以调用,非const对象也可以调用
const_iterator end()const
{
return const_iterator(_head);
}
front函数的作用是返回容器中第一个有效数据,对于front函数我们实现两个版本,一个是普通版本可读可写,另外一个是const版本只读的(非const对象可以调用,const对象也可以调用)
//可读可写
T& front()
{
return *begin();//返回第一个有效数据
}
const T& front()const
{
return *begin();
}
back函数的作用是返回容器中最后一个有效数据,同样对于back函数我们也实现两个版本,一个是普通版本可读可写,另外一个是const版本只读的(非const对象可以调用,const对象也可以调用)
//可读可写
T& back()
{
return *(--end());
}
const T& back()const
{
return *(--end());
}
insert函数的作用是在pos迭代器位置的前面插入一个结点
//在pos位置前面插入一个结点
void insert(iterator pos, const T& x)
{
assert(pos._pnode);
node* cur = pos._pnode;
//要插入的新结点
node* newnode = new node(x);
//找到pos迭代器上结点的前一个结点
node* prev = cur->_prev;
//改变三者的指向
//prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
}
erase函数的作用是删除pos迭代器位置的结点
//删除pos位置的结点
iterator erase(iterator pos)
{
assert(pos._pnode);
//不能删除头结点
assert(pos != end());
node* cur = pos._pnode;
//prev cur next
node* prev = cur->_prev;
node* next = cur->_next;
delete cur;
prev->_next = next;
next->_prev = prev;
//返回下一个位置的地址构造出的迭代器
return iterator(next);
}
push_back函数的作用是在容器的尾部插入一个新结点,由于我们的头结点的prev结点就是指向尾部的,因此尾插就相当于是在头结点的前面插入一个结点。
因为我们上面实现了insert函数,因此我们这里的push_back可以复用insert来实现
//尾插
void push_back(const T& x)
{
创建一个val值为x的新结点
//node* newnode = new node(x);
找到当前链表中的尾指针
//node* tail = _head->_prev;
_head tail newnode
//tail->_next = newnode;
//newnode->_prev = tail;
newnode来做新的尾
//_head->_prev = newnode;
//newnode->_next = _head;
insert(end(), x);
}
pop_back函数的作用是删除容器的尾部的结点,由于我们的头结点的prev结点就是指向尾部的,因此尾删就相当于是删除头结点的前驱结点。
因为我们上面实现了erase函数,因此我们这里可以通过复用erase函数来实现
//尾删
void pop_back()
{
//node* tail = _head->_prev;
为空了就不能删了
//assert(tail != _head);
//node* Tailprev = tail->_prev;
释放当前的尾指针
//delete tail;
Tailprev做新的尾
//Tailprev->_next = _head;
//_head->_prev = Tailprev;
erase(--end());
}
push_front函数的作用是头插也就是在第一个有效结点前插入一个结点。
我们这里同样可以通过复用insert函数来实现
//头插
void push_front(const T& x)
{
insert(begin(), x);
}
pop_front函数的作用是头删也就是删除第一个有效结点
我们这里同样可以通过复用erase函数来实现
//头删
void pop_front()
{
erase(begin());
}
size函数的作用是返回当前容器中的有效数据个数,因为list底层不是一块连续的物理空间,不能够像vector那样用finish-start直接获得有效数据个数。因此链表想要获得容器中的有效数据个数,必须得通过遍历的方式去统计数据的有效个数。
size_t size()
{
auto it = begin();
size_t sz = 0;
while (it != end())
{
++it;
sz++;
}
return sz;
}
clear函数的作用是用来清空list容器中的数据(头结点不能删),因此我们只需要通过迭代器遍历的方式依次删掉容器中除头结点以外的节点即可。
//清空数据
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
empty函数的作用是判断容器是否为空,因此我们只需要判断一下begin函数和end函数返回的迭代器是否是同一个位置的迭代器即可(如果是,则表示该list容器为空并且容器中只有一个头结点,如果不是则表示该容器不为空)
//判断容器是否为空
bool empty()const
{
return begin() == end();
}
swap函数的作用是交换两个存储相同数据类型的list容器的内容,由于list容器当中存储的实际就只有链表的头指针,因此我们只需要将两个容器的头指针进行交换,就可以达到交换两个list容器的效果。我们这里实现swap函数可以直接调用库(algorithm)里面的swap模板函数即可,我们如果想调用库里面的swap模板函数的话,需要在swap前面加上"::"(作用域限定符),告诉编译器优先去全局范围找swap函数,如果不加上作用域限定符编译器就会认为你调用的就是你正在实现的swap函数(就近原则)
//交换两个结点指针
void swap(list<T>& lt)
{
::swap(_head, lt._head);
}
namespace mlf
{
//结点类
template<class T>
struct _list_node
{
//成员变量
T _val;
_list_node<T>* _prev;
_list_node<T>* _next;
//构造函数
_list_node(const T& val = T())
:_val(val)
, _prev(nullptr)
, _next(nullptr)
{}
};
//迭代器类
/* typedef _list_iterartor iterator;
typedef _list_iterartor const_iterator;*/
template<class T, class Ref, class Ptr>
struct _list_iterator
{
typedef _list_node<T> node;
typedef _list_iterator<T, Ref, Ptr> self;
node* _pnode;
//构造函数
_list_iterator(node* pnode)
:_pnode(pnode)
{}
// 拷贝构造、operator=、析构我们不写,编译器默认生成就可以用
Ref operator*()
{
return _pnode->_val;
}
Ptr operator->()
{
return &_pnode->_val;
}
//++it -> it.operator++(&it)
self& operator++()
{
_pnode = _pnode->_next;
return *this;
}
//--it -> it.operator--(&it)
self& operator--()
{
_pnode = _pnode->_prev;
return *this;
}
//it++ -> it.operator++(&it,0)
self operator++(int)
{
self temp(*this);
_pnode = _pnode->_next;
return temp;
}
//it-- -> it.operator--(&it,0)
self operator--(int)
{
self temp(*this);
_pnode = _pnode->_prev;
return temp;
}
bool operator==(const self& lt)
{
return _pnode == lt._pnode;
}
bool operator!=(const self& lt)
{
return _pnode != lt._pnode;
}
};
// 这样写虽然可以,但是普通迭代器和const迭代器有大量重复冗余代码
// 如何优化呢?
//template
//struct _list_const_iterartor
//{
// typedef _list_node node;
// typedef _list_const_iterartor self;
// node* _pnode;
// _list_const_iterartor(node* pnode)
// :_pnode(pnode)
// {}
// // 拷贝构造、operator=、析构我们不写,编译器默认生成就可以用
// const T& operator*()
// {
// return _pnode->_val;
// }
// //operator->
// bool operator!=(const self& s) const
// {
// return _pnode != s._pnode;
// }
// bool operator==(const self& s) const
// {
// return _pnode == s._pnode;
// }
// // ++it -> it.operator++(&it)
// self& operator++()
// {
// _pnode = _pnode->_next;
// return *this;
// }
// // it++ -> it.operator++(&it, 0)
// self operator++(int)
// {
// self tmp(*this);
// _pnode = _pnode->_next;
// return tmp;
// }
// self& operator--()
// {
// _pnode = _pnode->_prev;
// return *this;
// }
// self operator--(int)
// {
// self tmp(*this);
// _pnode = _pnode->_prev;
// return tmp;
// }
//};
//list类
template<class T>
class list
{
typedef _list_node<T> node;
public:
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
//可读可写
//返回头结点的下一个结点的地址构造出来的迭代器
iterator begin()
{
return iterator(_head->_next);
}
//只读
//不修改最好加上const
//这样const对象可以调用,非const对象也可以调用
const_iterator begin()const
{
return const_iterator(_head->_next);
}
//可读可写
//返回头结点的地址构造出来的迭代器
iterator end()
{
return iterator(_head);
}
//只读
//不修改最好加上const
//这样const对象可以调用,非const对象也可以调用
const_iterator end()const
{
return const_iterator(_head);
}
//构造函数
list()
{
//_head = new node(T());
_head = new node;
_head->_next = _head;
_head->_prev = _head;
}
//拷贝构造
list(const list<T>& lt)
{
_head = new node;
_head->_next = _head;
_head->_prev = _head;
for (const auto& e : lt)
{
push_back(e);
}
}
赋值运算符重载
实现方法一
//list& operator=(const list& lt)
//{
// //先清空list里面的数据
// clear();
// //防止自己给自己赋值
// if (this != <)
// {
// for (const auto& e : lt)
// {
// push_back(e);
// }
// }
// return *this;
//}
//实现方法二
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
//析构函数
~list()
{
clear();
delete _head;//释放头结点
_head = nullptr;//将头结点置空
}
//可读可写
T& front()
{
return *begin();
}
const T& front()const
{
return *begin();
}
//可读可写
T& back()
{
return *(--end());
}
const T& back()const
{
return *(--end());
}
//头插
void push_front(const T& x)
{
insert(begin(), x);
}
//头删
void pop_front()
{
erase(begin());
}
//尾插
void push_back(const T& x)
{
创建一个val值为x的新结点
//node* newnode = new node(x);
找到当前链表中的尾指针
//node* tail = _head->_prev;
_head tail newnode
//tail->_next = newnode;
//newnode->_prev = tail;
newnode来做新的尾
//_head->_prev = newnode;
//newnode->_next = _head;
insert(end(), x);
}
//尾删
void pop_back()
{
//node* tail = _head->_prev;
为空了就不能删了
//assert(tail != _head);
//node* Tailprev = tail->_prev;
释放当前的尾指针
//delete tail;
Tailprev做新的尾
//Tailprev->_next = _head;
//_head->_prev = Tailprev;
erase(--end());
}
//在pos位置前面插入一个值
void insert(iterator pos, const T& x)
{
assert(pos._pnode);
node* cur = pos._pnode;
//要插入的新结点
node* newnode = new node(x);
//找到pos迭代器上结点的前一个结点
node* prev = cur->_prev;
//改变三者的指向
//prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
}
//删除pos位置的结点
iterator erase(iterator pos)
{
assert(pos._pnode);
//不能删除头结点
assert(pos != end());
node* cur = pos._pnode;
//prev cur next
node* prev = cur->_prev;
node* next = cur->_next;
delete cur;
prev->_next = next;
next->_prev = prev;
//返回下一个位置的地址构造出的迭代器
return iterator(next);
}
//清空数据
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
size_t size()
{
auto it = begin();
size_t sz = 0;
while (it != end())
{
++it;
sz++;
}
return sz;
}
//判断容器是否为空
bool empty()const
{
return begin() == end();
}
//交换两个结点指针
void swap(list<T>& lt)
{
::swap(_head, lt._head);
}
private:
node* _head;
};
}
vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:
vector | list | |
---|---|---|
底层结构 | 动态顺序表,一段连续的空间 | 带头结点的双向循环链表 |
随机访问 | 支持随机访问,访问某个元素效率O(1) | 不支持随机访问,访问某个元素效率O(N) |
插入和删除 | 任意位置插入和删除效率低,需要挪动数据,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空间,拷贝旧空间的数据,释放旧空间,导致效率更低 | 任意位置插入和删除效率高,不需要挪动数据,时间复杂度为O(N) |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭代器失效 | 在插入数据时,要给所有的迭代器重新赋值,因为插入数据有可能会导致重新扩容,致使原来的迭代器失效,删除时,当前迭代器需要重新赋值否则会失效 | 插入数据不会导致迭代器失效,删除数据时,只会导致当前迭代器失效,其他迭代器不受影响 |
使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |
以上就是list模拟实现的全部内容了,如果觉得对你有帮助的话,可以三连一波支持一下作者。