前言
STL list 容器,又称双向链表容器,即该容器的底层是以双向链表的形式实现的。这意味着,list 容器中的元素可以分散存储在内存空间里,而不是必须存储在一整块连续的内存空间中
文章目录
一. 基本框架
正如我们在学习数据结构的带头双向链表时,链表节点的结构体定义里,应该有指向下一个节点的next指针,指向上一个节点的prev指针,和该节点的值date
这是一个节点的,作为整体链表的框架,我们需要一个哨兵卫的头结点head。其内部不存储数据,仅有链接头尾的作用。
代码如下
//节点的结构体
template<class T>
struct list_node
{
list_node<T>*_next;
lsit_node<T>*_prev;
T _date;
};
//链表的框架
template<class T>
class list
{
typedef list_node<T> node;
public:
private:
node* head;
};
因为list同样要和vector一样,适配不同的数据类型,所以我们采用类模板,结构体处也需要使用模板,这样才可以统一参数。
另外为了见名知意,所以我们也可以对一些较长的,较复杂的参数名进行重命名。
二. 数据写入&遍历
1. 数据写入
定义好基本框架后,接下来我们需要可以实例化对象完成数据存储。
那么实例化对象需要构造函数
//list_node的默认构造
template<class T>
struct list_node
{
list_node<T>*_next;
list_node<T>*_prev;
T _date;
list_node(const T&x=T())
:_next(nullptr)
, _prev(nullptr)
, _date(x)
{}
};
//list的构造函数
list()
{
_head = new node;
_head->next = _head;
_head->prev = _head;
}
因为是实例化的是哨兵卫的头结点,所以最开始,我们让_next和_prev指向自己
对于最开始的写入,我们先编写最简单的尾插
void push_back(const T&date)
{
node*tail = _head->prev;
node*new_node = new node(date);
//断开旧尾和头的链接
tail->_next = new_node;
new_node->_prev = tail;
//链接新尾
new_node->_next = head;
head->_prev = new_node;
}
简单测试一下
void list_test1()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
}
这就是最简单的尾插
2. 数据遍历
那么接下来,我们如何遍历这个list呢?
根据STL一贯作风,我们需要迭代器
所以我们需要封装一下迭代器。
(1). 迭代器
首先,迭代器本质是指针,所以其内部变量应该是list_node的指针
基本的构造函数,然后我们使用迭代器遍历需要*解引用,++迭代器,迭代器!=end
迭代器又是封装,所以这些运算符无法直接使用,所以我们需要对他们进行重载
代码如下
template<class T>
struct _list_iterator
{
typedef list_node<T> node;
typedef _list_iterator<T> self;//因为需要返回迭代器,所以重命名一下
//成员变量
node *_node;
//构造函数
_list_iterator(node* x)
:_node(x)
{}
//operator*重载
T&operator*()
{
return _node->_date;
}
//operator++重载
self operator++()
{
_node = _node->_next;
return *this;//返回迭代器本身
}
//operator!=重载
bool operator!=(const self&s)
{
return _node != s._node;
}
};
接下来,我们需要将迭代器包含在list中,并且是公有,这样才能直接使用。
然后就像以往迭代器遍历,我们还需要begin()和end()
//重命名
typedef _list_iterator<T> iterator;
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
因为迭代器访问是左闭右开,所以end应该返回哨兵卫的头结点,这样最后一个元素才可以被访问到
测试
void list_test1()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
遍历需要++it,不能it++,因为operator++参数列表没有参数是重载前置++
(2). 零碎知识点
在以往的STL模拟实现里,深浅拷贝一直难点。这处,基于我们目前编写的代码,明显是一个浅拷贝。但是程序却没有崩溃。这是为什么呢?
原因是:因为我们并没有写迭代器的析构函数,所以程序结束时,系统会调用默认析构,而默认析构只有将it=NULL置空这样的操作,并没有delete释放迭代器指向的空间。所以不存在重复释放空间的操作。
这里也能说明,迭代器的功能之一是支持我们对封装的数据做访问,遍历,只是一个工具,并不具备操作数据存储的空间的权限。
(3). 其他重载
//operator后置++重载
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
//operator前置--重载
self& operator --()
{
_node = _node->_prev;
return *this;//返回迭代器本身
}
//operator后置--重载
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
//operator==重载
bool operator ==(const self&s)
{
return _node == s._node;
}
三. const 迭代器
函数传参,我们必定会用到const list<,那么const的迭代器就是我们需要实现的。
注意:const迭代器的定义不能是以下形式
因为const修饰的是T*,导致使得指针变成常量,也就是指针常量,
那么如何实现呢?
其实我们可以重新定义一个类,内部属性和list_iterator几乎一样。然后在operator*等代码更改返回值就好
template<class T>
struct _list_const_iterator
{
typedef list_node<T> node;
typedef _list_const_iterator<T> self;//因为需要返回迭代器,所以重命名一下
//成员变量
node* _node;
//构造函数
_list_const_iterator(node* x)
:_node(x)
{}
//operator*重载
const T&operator*()
{
return _node->_date;
}
//operator前置++重载
self& operator ++()
{
_node = _node->_next;
return *this;//返回迭代器本身
}
//operator后置++重载
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
//operator前置--重载
self& operator --()
{
_node = _node->_prev;
return *this;//返回迭代器本身
}
//operator后置--重载
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
//operator==重载
bool operator ==(const self&s)
{
return _node == s._node;
}
//operator!=重载
bool operator!=(const self&s)
{
return _node != s._node;
}
};
注意:operator*的返回值要加const,因为要限制引用内部的值不能修改
list类内也要加相应的,返回const迭代器的begin 和end函数
但是这样代码有些冗余了,两个迭代器只有operator*的返回值不一样。
此处,我们使用两个模板参数,解决这个问题
代码如下
然后list中迭代器的定义是这样的
因为模板会自动匹配类型,当我们调用const_iterator时,_list_iterator中的Ref就是const T&,然后operator返回值就是const T&了。
而当我们调用iterator时,_list_iterator中的Ref是T&,operator返回值是T&。
完美解决问题。
测试
void print(const list<int><)
{
list<int>::const_iterator it= lt.begin();
while (it != lt.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
1. 迭代器参数模板的第三个参数
以上,我们只测试了int内置数据类型,但list的模板参数还可以是自定义数据类型
比如,我们定义一个AA类,将其作为参数模板实例化一个list,并对其进行遍历。
struct AA
{
int _a1;
int _a2;
AA(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
};
void list_test2()
{
list<AA>lt;
lt.push_back(AA(1, 1));
lt.push_back(AA(2, 2));
lt.push_back(AA(3, 3));
list<AA>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
但程序无法运行,因为我们并没有重载流插入
解决方法之一呢,就是在全局域,重载一个operator<<AA的函数。
解决方法二,就是先*解引用,再 . 访问
这里还有一个小知识点
这里我们如果运行,编译是不通过的
原因是因为,AA类没有写无参构造
因为list_node的构造需要使用匿名对象,而匿名对象需要调用相应数据类型的无参构造,但AA类没有无参构造,所以报错。
我们只需要在AA内的构造函数提供全缺省就好了
程序成功运行
但是,迭代器模拟的是指针,指针如果是自定义数据类型的指针,那么他的访问方式应该是 - >。所以,为了提高代码的可读性,我们需要重载operator->
那么如何操作呢?其实只要返回_date的指针就好了
但是这就有一点奇怪了,因为operator->不是应该先->,然后返回_date的指针,然后再->解引用?
这是其实是编译器为了可读性进行了优化。
但是考虑到还有const的返回值,函数又不能凭借返回值实现重载,所以类模板的参数列表还可以使用第三个参数
四. 插入&删除
list常用的函数接口就是插入和删除,这样也可以囊括头插和头删。
1. 插入
void insert(iterator pos, const T&x)
{
node*cur = pos._node;
node*prev = cur->_prev;
node*new_node = new node(x);
prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;
}
后续我们通过find返回指定位置的迭代器,就可以实现O(1)的插入删除
测试
void list_test3()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
auto pos = lt.begin();
pos++;
lt.insert(pos, 20);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
在2位置前插入20
这样头插尾插就可以复用insert了
void push_back(const T&date)
{
//复用insert
insert(end(), date);
}
void push_front(const T&date)
{
insert(begin(), date);
}
测试
void list_test4()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
lt.push_front(10);
lt.push_front(20);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
list的insert并不会造成迭代器失效,因为没有改变指针指向
2. 删除
iterator erase(iterator pos)
{
assert(pos != end());
node*cur = pos._node;
node*prev = cur->_prev;
node*next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
cur = nullptr;
return iterator(next);
}
测试
void list_test5()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
auto pos = lt.begin();
pos++;
lt.erase(pos);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
但是,需要注意的是,哨兵卫的头结点不可以被删除,因为一旦删除,那整个链表都无法找到
所以可以加一个断言
而且为了支持连续删除,我们还可以将返回值设为删除节点的下一个节点的指针
assert(pos != end());
头删尾删
void pop_front()
{
erase(begin());
}
void pop_back()
{
erase(--end());
}
测试
void list_test6()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
//先尾删
lt.pop_back();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
//再头删
lt.pop_front();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
五. 析构函数
list的成员变量只有哨兵卫的头结点,要释放链表,我们需要先释放链表的节点,最后再释放哨兵卫的头结点
void clear()
{
iterator it = begin();
while (it != end())
{
//it = erase(it);
erase(it++);
}
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
clear中,两种删除节点的方式都可以
第一种是删除当前节点,然后再用 it 接收下一个节点
第二种是复用后置++,返回的是要删除节点迭代器的拷贝,it迭代器其实已经指向下一个节点了
六. 构造函数
除了无参构造,STL的构造方式一般还有迭代器区间构造和拷贝构造
1. 迭代器区间构造
因为要适配各种数据类型的迭代器,不仅仅是内置类型,还有string,vector等的自定义类型。所以迭代器区间构造需要使用函数模板。
另外,再插入元素之前,我们还需要构造出哨兵卫的头结点。这时我们发现,不是只有无参构造需要构造哨兵卫的头结点,所以我们可以封装一个初始化的函数,并且在无参构造和迭代器区间构造复用
void empty_init()
{
_head = new node;
_head->_next = _head;
_head->_prev = _head;
}
//无参构造
list()
{
empty_init();
}
//迭代器区间构造
template<class Iterator>
list(Iterator first, Iterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
测试
void list_test7()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
list<int>lt2(lt1.begin(), lt1.end());
for (auto e : lt2)
{
cout << e << " ";
}
cout << endl;
}
2. 拷贝构造
同vector的模拟实现,我们可以复用迭代器区间构造和swap函数,这样可以使得代码简洁一下,底层都是一个一个拷贝
void swap(list<T>&tmp)
{
std::swap(_head, tmp._head);
}
//拷贝构造
list(const list<T><)
{
empty_init();
//现代写法,复用迭代器区间构造和swap函数
list<T>tmp(lt.begin(), lt.end());
swap(tmp);
}
3. operator=
list<T>&operator=(list<T> lt)
{
swap(lt);
return *this;
}
注意:此处传参不可以是&引用,因为swap会实现交换,如果用引用的话,原本的值会被改变,所以只能传参使用
七. 完整代码
//节点的结构体
template<class T>
struct list_node
{
list_node<T>*_next;
list_node<T>*_prev;
T _date;
list_node(const T&x=T())
:_next(nullptr)
, _prev(nullptr)
, _date(x)
{}
};
template<class T,class Ref,class Ptr>
struct _list_iterator
{
typedef list_node<T> node;
typedef _list_iterator<T,Ref,Ptr> self;//因为需要返回迭代器,所以重命名一下
//成员变量
node* _node;
//构造函数
_list_iterator(node* x)
:_node(x)
{}
//operator*重载
Ref&operator*()
{
return _node->_date;
}
Ptr operator->()
{
return &_node->_date;
}
//operator前置++重载
self& operator ++()
{
_node = _node->_next;
return *this;//返回迭代器本身
}
//operator后置++重载
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
//operator前置--重载
self& operator --()
{
_node = _node->_prev;
return *this;//返回迭代器本身
}
//operator后置--重载
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
//operator==重载
bool operator ==(const self&s)
{
return _node == s._node;
}
//operator!=重载
bool operator!=(const self&s)
{
return _node != s._node;
}
};
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;
/*typedef const iterator const_iterator;*/
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);
}
void empty_init()
{
_head = new node;
_head->_next = _head;
_head->_prev = _head;
}
//无参构造
list()
{
empty_init();
}
//迭代器区间构造
template<class Iterator>
list(Iterator first, Iterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
void swap(list<T>&tmp)
{
std::swap(_head, tmp._head);
}
//拷贝构造
list(const list<T><)
{
empty_init();
传统写法,一个一个拷贝
//list<T>::const_iterator it = lt.begin();
//while (it != lt.end())
//{
// push_back(*it);
// ++it;
//}
//现代写法,复用迭代器区间构造和swap函数
list<T>tmp(lt.begin(), lt.end());
swap(tmp);
}
list<T>&operator=(list<T> lt)
{
swap(lt);
return *this;
}
void push_back(const T&date)
{
//node*tail = _head->_prev;
//node*new_node = new node(date);
断开旧尾和头的链接
//tail->_next = new_node;
//new_node->_prev = tail;
链接新尾
//new_node->_next = _head;
//_head->_prev = new_node;
//复用insert
insert(end(), date);
}
void push_front(const T&date)
{
insert(begin(), date);
}
void insert(iterator pos, const T&x)
{
node*cur = pos._node;
node*prev = cur->_prev;
node*new_node = new node(x);
prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;
}
iterator erase(iterator pos)
{
assert(pos != end());
node*cur = pos._node;
node*prev = cur->_prev;
node*next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
cur = nullptr;
return iterator(next);
}
void pop_front()
{
erase(begin());
}
void pop_back()
{
erase(--end());
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
erase(it++);
}
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
private:
node* _head;
};
结束语
本章list的模拟实现就先到这了,反向迭代器将单独出一篇博客,感谢阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要