看源码观察结构
由源码可以得知,list的底层是带头双向循环链表
—
结点类模拟实现
list实际上是一个带头双向循环链表,要实现list,则首先需要实现一个结点类,而一个结点需要存储的信息为:数据、前驱指针、后继指针
而对于该结点类的成员函数来说,我们只需实现一个构造函数即可,因为该结点类只需要根据数据来构造一个结点即可,而结点的释放则由list的析构函数来完成,
基本结构
//结点类 -> struct类型,内容公有
//结点类模板参数
template<class T>
struct ListNode
{
//成员函数
ListNode(const T& val = T()); //构造函数
//成员变量
T _val; //数据域
ListNode<T>* _next; //后继指针
ListNode<T>* _prev; //前驱指针
};
构造函数
构造函数直接根据所给数据构造一个结点即可,构造出来的结点的数据域存储的就是所给数据,而前驱指针和后继指针均初始化为空指针即可
//构造函数 err版本
ListNode(const T& x)
{
_val = x;
_next = nullptr;
_prev = nullptr;
}
提供默认参数才合适
若构造结点时未传入数据,则默认以list容器所存储类型的默认构造函数所构造出来的值为传入数据,如果是string就是空串,如果是int就是0
//这样写更好 默认参数是一个匿名对象
ListNode(const T& val = T())
:_val(val)
,_prev(nullptr)
,_next(nullptr)
{}
迭代器类模拟实现
关于迭代器的说明
迭代器有两种实现方式,具体应根据容器底层数据结构实现:
- 原生态指针,比如:vector和string ->物理空间是连续的
- 因为string和vector对象都将其数据存储在了一块连续的内存空间,我们通过指针进行自增、自减以及解引用等操作,就可以对相应位置的数据进行一系列操作,因此string和vector当中的迭代器就是原生指针,
2.将原生态指针进行封装,因迭代器使用形式与指针完全相同,因此在自定义的类中必须实现以下
方法:
- 指针可以解引用,迭代器的类中必须重载
operator*()
- 指针可以通过->访问其所指空间成员,迭代器类中必须重载
oprator->()
- 指针可以++向后移动,迭代器类中必须重载
operator++()
与operator++(int)
至于operator--()
/operator--(int)
是否需要重载,根据具体的结构来抉择,双向链表可
以向前 移动,所以需要重载,如果是forward_list就不需要重载– - 迭代器需要进行是否相等的比较,因此还需要重载
operator==()
与operator!=()
但是对于list来说,其各个结点在内存当中的位置是随机的,并不是连续的,我们不能仅通过结点指针的自增、自减以及解引用等操作对相应结点的数据进行操作,
迭代器的意义就是,让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问,
list的结点指针的行为不满足迭代器定义,那么我们可以对这个结点指针进行封装,对结点指针的各种运算符操作进行重载
总结: list的迭代器 实际上就是对结点指针进行了封装,对其各种运算符进行了重载,使得结点指针的各种行为看起来和普通指针一样,(例如,对结点指针自增就能指向下一个结点 p = p->next)
接口函数
list的迭代器是个结点(对象),里面封装了一个指向链表结点的指针
//迭代器的类模板参数
//如果是const迭代器: Ref就是const T& Ptr就是const T*
//如果是普通迭代器:Ref就是T& Ptr就是T*
template<class T, class Ref, class Ptr>
struct _list_iterator
{
typedef ListNode<T> ListNode;//结点类型
typedef _list_iterator<T, Ref, Ptr> self;//迭代器类型
_list_iterator(ListNode* 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->();
//成员变量
ListNode* _pnode; //一个指向结点的指针
};
迭代器模板参数说明
template<class T, class Ref, class Ptr>
typedef _list_iterator<T, Ref, Ptr> self;//重命名迭代器类型为self
迭代器类的模板参数列表当中的Ref和Ptr分别代表的是引用类型和指针类型
当我们使用普通迭代器时,编译器就会用类模板实例化出一个普通迭代器对象;当我们使用const迭代器时,编译器就会用类模板实例化出一个const迭代器对象
不设计三个模板参数,那么就不能很好的区分普通迭代器和const迭代器,因为普通对象的迭代器可读可写,而const对象的迭代器 只可读不可写!
构造函数
迭代器类实际上就是对结点指针进行了封装
其成员变量就是结点指针,所以其构造函数直接根据所给结点指针构造一个迭代器对象即可,
//构造函数
//直接根据所给结点指针构造一个迭代器对象
_list_iterator(ListNode* pnode)
:_pnode(pnode)
{}
关于拷贝构造等函数的说明:
拷贝构造,operator,析构函数我们都不需要写,因为成员变量是内置类型(指针), 用编译器默认生成的就可以
list<int>:: iterator it = lt,begin();
注意:这里不是赋值,而是迭代器的拷贝构造. 使用浅拷贝足矣,成员变量是指针,是类型,编译器默认生成的就是浅拷贝
链表的结点不属于迭代器管,不需要写析构函数, 迭代器只是去访问修改即可.结点是属于链表的
注意:我们把当前迭代器对象的类型_list_iterator<T, Ref, Ptr>
typedef为:self
注意: return *this返回的是当前对象的克隆或者本身(若返回类型为A, 则是克隆, 若返回类型为A&, 则是本身 ),return this返回当前对象的地址(指向当前对象的指针)
++运算符重载
前置++
前置++原本的作用是将数据自增,然后返回自增后的数据,
而对于结点迭代器的前置++:应该先让结点指针指向后一个结点.然后再返回“自增”后的结点迭代器即可
让结点迭代器的行为看起来更像普通指针
//前置++
self& operator++()
{
_pnode = _pnode->_next;//当前迭代器结点中的指针指向下一个结点
return *this;//返回自增后的迭代器
}
后置++
后置++,先拷贝构造当前迭代器结点, 然后让当前迭代器结点的指针自增指向下一个结点,最后返回“自增”前的结点迭代器即可,
//后置++
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;//返回自减前的迭代器
}
==运算符重载
想知道的是这两个迭代器是否是同一个位置的迭代器-> 判断这两个迭代器当中的结点指针的指向是否相同即可
//比较两个迭代器是否相同
bool operator==(const self& s) const
{
return _pnode == s._pnode;//直接比较两个迭代器的指针是否一样即可
}
!=运算符重载
想知道的是这两个迭代器是否 不是同一个位置的迭代器-> 判断这两个迭代器当中的结点指针的指向是否相同即可
bool operator!=(const self& s) const
{
return _pnode != s._pnode;//直接比较两个迭代器的指针是否不一样即可
}
*运算符重载
使用解引用操作符时,是想得到该指针指向的数据内容
因此,我们直接返回当前结点指针所指结点的数据即可,这里需要使用引用返回,因为解引用后可能需要对数据进行修改,
Ref operator*()
{
return _pnode->_val;//返回迭代器的指针指向结点的数据
}
->运算符重载
->返回当前迭代器结点的指针所指结点的数据的地址
Ptr operator->()
{
return &_pnode->_val; //返回迭代器的指针所指结点的数据的地址
}
应用场景
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
int main()
{
Mango::list<Date> lt;
Date d1(2022, 1, 1);
Date d2(2022, 1, 2);
lt.push_back(d1);
lt.push_back(d2);
auto it = lt.begin();
while (it != lt.end())
{
/*cout << (*it)._year <<" "<<(*it)._month<< " "<< (*it)._day<<endl;*/
cout << it->_year << " " << it->_month << " " << it->_day << endl;
it++;
}
}
*it
得到的是日期类对象, 然后用.
访问成员
当然也可以用->
直接访问成员
list的模拟实现
接口函数
//模拟实现list
template<class T>
class list
{
public:
typedef ListNode<T> ListNode;
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
//默认成员函数
list();
list(size_t n, const T& val = T())
list(int n, const T& val = T())
template<class InputIterator>//取名为InputIterator说明可以用任意类型的迭代器构造
list(InputIterator first, InputIterator last)
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 resize(size_t n, const T& val = T());
void clear();
bool empty() const;
void swap(list<T>& lt);
private:
ListNode* _head; //指向链表头结点的指针
};
默认成员函数
构造函数1-默认构造函数
list是一个带头双向循环链表,我们需要先构造一个头结点,并让其前驱指针和后继指针都指向自己
//构造函数
list()
{
_head = new ListNode;//先新开一个结点
//哨兵位自己指向自己
_head->_prev = _head;
_head->_next = _head;
}
构造函数2-用n个值相同的值初始化
//注意:这里的val要给缺省值,不能给0之类的,因为T的类型未知
list(size_t n, const T& val = T())
{
//新建一个哨兵位
_head = new ListNode();
_head->_next = _head;
_head->_prev = _head;
for (size_t i = 0; i < n; i++)
{
push_back(val); //复用push_back接口
}
}
构造函数3-迭代器区间初始化
//使用迭代器区间初始化
template<class InputIterator>//取名为InputIterator说明可以用任意类型的迭代器构造
list(InputIterator first, InputIterator last)
{
_head = new ListNode();
_head->_next = _head;
_head->_prev = _head;
while (first != last)
{
push_back(*first);//复用push_back
first++;
}
}
上述两个构造函数可能存在冲突
存在冲突的例子:
void test_list0()
{
Mango::list<int> lt(5, 2);//报错,非法的间接寻址
for (auto e : lt)
{
cout << e << " ";
}
Mango::list<Date> lt2(5,Date(2022,1,1));//没问题
cout << endl;
}
我们本意是想用5个2初始化lt,但是调用的是迭代器区间初始化,因为迭代器区间里存在解引用,所以会报错间接寻址.
同样的,vector也会存在这个问题!解决方法也一样,看下面讲解
但是用5个Date对象初始化lt2却没有问题,调用的是n个值初始化的构造函数
我们首先要知道为什么会有这个错误
编译器的原则是有更匹配的就去找更匹配的,有现成的就不去推演模板
如何修改呢?为n个值初始化的构造函数增设一个int版本的
//使用n个相同的值初始化
list(int n, const T& val = T())
{
//新建一个哨兵位
_head = new ListNode();
_head->_next = _head;
_head->_prev = _head;
for (int i = 0; i < n; i++)
{
push_back(val); //复用push_back接口
}
}
构造函数4-初始化列表初始化
方法1:迭代器遍历插入
//使用初始化列表初始化 list<int> lt{1,2,3,4};
//需要引用#include<initializer_list>函数
list(initializer_list<T> ilt)
{
_head = new ListNode();
_head->_next = _head;
_head->_prev = _head;
initializer_list<T>::iterator it = ilt.begin();
while(it!=ilt.end())
{
push_back(*it);//复用push_back函数
it++;
}
}
方法2:范围for
//使用初始化列表初始化 list<int> lt{1,2,3,4};
//需要引用#include<initializer_list>函数
list(initializer_list<T> ilt)
{
_head = new ListNode();
_head->_next = _head;
_head->_prev = _head;
for (auto& e : ilt)
{
push_back(e);
}
}
方法3:现代写法
注意:必须要构造哨兵位节点,否则就把随机值给了tmp, tmp出了作用域调用析构函数,析构函数里面会调用clear函数,会去访问迭代器释放节点,会导致崩溃
list(initializer_list<T> ilt)
{
//构造哨兵位节点
_head = new ListNode();
_head->_next = _head;
_head->_prev = _head;
list<T> tmp(ilt.begin(),ilt.end());//复用迭代器初始化的函数,构造出临时对象
std::swap(_head,tmp._head);//交换哨兵位节点指针
}
拷贝构造函数
根据所给list容器,直接拷贝构造出一个相同的list对象,
对于拷贝构造函数,我们也是要先申请一个头结点,并让其前驱指针和后继指针都指向自己
然后将所给容器当中的数据,通过遍历的方式一个个尾插到新构造的容器后面即可
传统写法:
//拷贝构造函数
//l2(l1)// l2.list(l1)
list(const list<T>& lt)
{
_head = new ListNode;//申请一个哨兵位结点
_head->_next = _head;
_head->_prev = _head;
//把lt的数据尾插到当前调用对象的容器
for (const auto& e : lt)
{
push_back(e);//将容器lt当中的数据一个个尾插到新构造的容器后面
}
}
现代写法:
// 拷贝构造 - 现代写法
// lt2(lt1)
list(const list<T>& lt)
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
list<T> tmp(lt.begin(), lt.end());//迭代器区间构造
::swap(_head, tmp._head);//交换哨兵位指针
}
注意必须给一个哨兵位节点,否则_head是一个随机值,换给tmp后出作用域调用析构函数,clear时获取begin()要解引用 _head-> _next会崩溃
赋值运算符重载函数
传统写法
先调用clear函数将原容器清空,然后将容器lt当中的数据,通过遍历的方式尾插到清空后的容器当中即可,
为了减少拷贝,传参传引用
//赋值重载函数
//lt2 = lt;//lt2.operator=(lt)
list<T>& operator=(const list<T>& lt)
{
//防止自己给自己赋值->地址比较
if (this != <)
{
clear();//先清除原来有的内容,但是头结点 不会清除
//把内容拷贝过去
for (const auto& e : lt)
{
push_back(e);
}
}
return *this;//为了支持连续赋值
}
现代写法
不使用引用接收参数
通过编译器自动调用list的拷贝构造函数构造出来一个list临时对象,然后调用swap函数将原容器与该临时的list对象进行交换即可
因为lt是临时对象,所以当该赋值运算符重载函数调用结束时,lt对象会自动销毁,并调用其析构函数进行清理,
//现代写法
//注意这里是传值
list<T>& operator=(list<T> lt)
{
//两个list的内容直接交换
swap(lt);//使用我们自己写的swap函数 ->::swap(_head, lt._head);//直接交换两个容器的哨兵位即可
return *this;//为了支持连续赋值
}
析构函数
先调用clear函数清理容器当中的数据,然后将头结点释放,最后将头指针置空即可,
//析构函数
~list()
{
clear();//先清空原来的内容
delete _head;//释放哨兵位
_head = nullptr;//哨兵位指针置空
}
迭代器
begin()和end()
begin函数返回的是第一个数据的迭代器,end函数返回的是最后一个有效数据的下一个位置的迭代器->即哨兵位的迭代器
直接把结点的地址传过去即可,通过结点的地址构造出一个迭代器
//迭代器
iterator begin()
{
//返回使用头结点的地址构造出来的普通迭代器
/* iterator tmp = iterator(_head->_next);
return tmp;*/
return iterator(_head->_next);
}
iterator end()
{
//返回使用哨兵位结点的地址构造出来的普通迭代器
return iterator(_head);
}
const_iterator begin() const
{
//返回使用头结点的地址构造出来的const迭代器
return const_iterator(_head->_next);
}
const_iterator end() const
{
//返回使用哨兵位结点的地址构造出来的const迭代器
return const_iterator(_head);
}
增删查改
front
获取第一个数据的内容
begin()迭代器返回的就是第一个数据的地址
T& front()
{
return *begin();//返回第一个数据的引用
}
const T& front() const
{
return *begin(); //返回第一个数据的const引用
}
back
获取最后一个数据的内容
end()迭代器返回的是哨兵位的地址 --end()返回的就是最后一个数据的地址
T& back()
{
return *(--end());//返回尾结点数据的引用
}
const T& back() const
{
return *(--end()); //返回最后一个有效数据的const引用
}
insert
insert函数可以在所给迭代器pos之前插入一个新结点,
1.先根据所给迭代器pos得到该位置处的结点指针cur 2.然后通过cur指针找到前一个位置的结点指针prev
- 根据所给数据x构造一个新结点 4.cur prev 新节点三者链接
注意**:pos可以是哨兵位的迭代器 ->这样相当于尾插** 但是pos迭代器结点中的指针不能为空
//pos迭代器位置前插入
iterator insert(iterator pos, const T& x)
{
//插入时:pos可以是哨兵位,相当于尾插
//pos是对象, 访问成员/成员函数用.访问
assert(pos._pnode);//迭代器结点中的指针不能为空
ListNode* cur = pos._pnode;
ListNode* prev = cur->_prev;
ListNode* newnode = new ListNode(x);
//prev newnode cur 链接
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
//这里是构造一个匿名对象返回,用newnode这个节点,构造一个迭代器,list的迭代器是一个结构体
//该结构体有一个指针指向这个newnode节点
return iterator(newnode);//返回新插入位置的迭代器
}
erase
erase函数可以删除所给迭代器位置的结点,
注意**:pos不可以是哨兵位的迭代器,即不能删除哨兵位 pos迭代器结点中的指针不能为空**
1.根据所给迭代器得到该位置处的结点指针cur 2.通过cur指针找到前一个位置的结点指针prev,以及后一个位置的结点指针next
3.紧接着释放cur结点,最后prev和next结点进行链接
//删除pos迭代器位置
iterator erase(iterator pos)
{
assert(pos._pnode);//迭代器中的节点指针不能为空
assert(pos != end());//不能删除哨兵位
ListNode* cur = pos._pnode;
ListNode* prev = cur->_prev;
ListNode* next = cur->_next;
delete cur;//释放cur结点 ->外部的pos迭代器的指针成为野指针
//prev next链接
prev->_next = next;
next->_prev = prev;
return iterator(next); //返回删除位置pos的下一个迭代器
}
关于insert和erase迭代器失效问题
问:insert之后, pos迭代器是否失效
insert不会导致迭代器失效,因为pos迭代器中的节点指针仍然指向原来的节点
问:erase之后, pos迭代器是否失效
一定失效,因为此时pos迭代器中的节点指针指向的节点已经被释放了,该指针相当于是野指针
push_back
可以复用insert函数,也可以自己写
复用: 尾插:即在哨兵位的前面插入 -> insert(end(), x)
自己写: 1.新建一个结点 2.记录原来的最后一个结点tail 3.新节点,tail ,哨兵位 三个结点进行链接
//尾插
//没有结点也适用
void push_back(const T& x)
{
//ListNode* newnode = new ListNode(x);/a新建一个结点
//ListNode* tail = _head->_prev;//哨兵位的_prev指向的就是尾结点
_head tail newnode 三者链接
//_head->_prev = newnode; //新节点成为新的尾
//tail->_next = newnode;
//newnode->_prev = tail;
//newnode->_next = _head;
//insert:在pos位置前插入
insert(end(), x);//在哨兵位前插入->相当于尾插
}
pop_back
尾删:即删除哨兵位的前一个结点 begin()返回的就是第一个结点的迭代器
//尾删
void pop_back()
{
//end()返回的是哨兵位的迭代器 --end()就是 _head->_prev 尾结点的迭代器
erase(--end());
}
push_front
复用:头插:即在第一个迭代器的位置前面插入
自己写:1.新建一个结点 2.记录原来的第一个结点phead 3.哨兵位 新节点 phead三个结点进行链接
//头插
//没有结点也适用
void push_front(const T& x)
{
//ListNode* newnode = new ListNode(x);//新建一个结点
//ListNode* phead = _head->_next;//哨兵位的_next指向的就是头结点
_head newnode phead
//_head->_next = newnode;//新节点成为新的头节点
//newnode->_prev = _head;
//newnode->_next = phead;
//phead->_prev = newnode;
insert(begin(),x);
}
pop_front
头删:即删除第一个结点->begin()返回的就是第一个结点的迭代器
//头删
void pop_front()
{
//begin()就是头结点
erase(begin());
}
其它函数
size
size函数用于获取当前容器当中的有效数据个数,因为list是链表,所以只能通过遍历的方式逐个统计有效数据的个数,
//长度
size_t size()
{
//遍历统计
size_t sz = 0;
iterator it = begin();
while (it != end())
{
++sz;
++it;
}
return sz;
}
第二种方式:给list对象多设置一个成员变量size,用于记录当前容器内的有效数据个数,
private:
ListNode* _head; //指向链表头结点的指针
size_t size; //记录链表的长度
clear
clear函数用于清空容器
通过遍历的方式逐个删除结点,只保留哨兵位即可
注意:要将哨兵位重新直接自己,否则再次插入会导致错误,_head指向已经释放的节点
写法1:
void clear()
{
iterator it = begin();
while (it != end())
{
//相当于先干掉当前位置迭代器的节点,然后it++
iterator del = it++;//返回++之前的迭代器,删了之后,it往后走
delete del._node; //也可以写成erase(del)
}
//哨兵位节点指向自己
_head->_next = _head;
_head->_prev = _head;
}
写法2:
//清空内容
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);//erase会返回删除位置的下一个迭代器
}
//哨兵位重新直接自己,否则再次插入会导致错误,_head指向已经释放的节点
_head->_prev =_head;
_head->_next = _head;
}
可以简写为:
//清空内容
void clear()
{
iterator it = begin();
while (it != end())
{
erase(it++);
}
//哨兵位重新直接自己,否则再次插入会导致错误,_head指向已经释放的节点
_head->_prev =_head;
_head->_next = _head;
}
empty
判断list是否为空
直接判断该容器的begin函数和end函数所返回的迭代器是否是同一个位置的迭代器即可,
(如果相等此时说明容器当中只有一个哨兵位结点)
bool empty() const
{
return begin() == end();
}
swap
swap函数用于交换两个list,list容器当中的成员变量只有指向哨兵位结点的指针,我们将这两个容器当中的哨兵位指针交换即可,
void swap(list<T>& lt)
{
::swap(_head, lt._head);//直接交换两个容器的哨兵位即可,此处用的是全局域std命名空间里面的swap函数
}
resize
case1:若当前容器的size小于所给n,则尾插结点,直到list的size等于n为止,
case2:若当前容器的size大于所给n,则只保留前n个数据,
低效做法
调用size()函数得到此时list的有效数据个数 和n进行对比
如果是case1: 找到尾结点的迭代器位置, 然后在后面尾插数据
如果是case2:从第一个结点(_head->next)开始遍历list,找到第n结点的迭代器,然后用循环释放后面的结点
调用size()需要遍历链表,时间复杂度就是O(n),如果结果是size大于n,那么还需要遍历list,效率低
高效方法:
设置一个变量len,用于记录当前所遍历的数据个数,然后开始遍历容器list,在遍历过程中:
- 当len大于或是等于n时遍历结束,此时说明该结点后的结点都应该被释放,将之后的结点都释放即可,
- 当容器遍历完毕时遍历结束,此时说明容器当中的有效数据个数小于n,则需要尾插结点,直到容器当中的有效数据个数为n时停止尾插即可,
//调整容器的大小为n
void resize(size_t n, const T& val = T())
{
iterator it = begin();//第一个数据的迭代器
size_t len = 0;//记录遍历到的数据个数
while (len < n && it != end())
{
++len;
++it;
}
//跳出循环 -有两种情况:case1:len == n 或者 case2 :已经到达了end()位置
//case1:len == n -> 说明是减少数据个数
if (len == n)
{
//后面位置的数据都删除掉
while (it != end())
{
it = erase(it);
}
}
//case2:len < n ->说明是扩容的情况
else
{
//从len位置开始往后扩容,扩容为n个
while (len < n)
{
push_back(val);
len++;
}
}
}
list.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace Mango
{
//结点类 -> struct类型,内容公有
template<class T>
struct ListNode
{
T _val;//数据域
ListNode<T>* _next;//后继指针
ListNode<T>* _prev;//前驱指针
//构造函数
//ListNode(const T& x)
//{
// _val = x;
// _next = nullptr;
// _prev = nullptr;
//}
//这样写更好 默认参数是一个匿名对象
ListNode(const T& val = T())
:_val(val)
,_prev(nullptr)
,_next(nullptr)
{}
};
//迭代器类
template<class T,class Ref,class Ptr>
struct _list_iterator
{
typedef ListNode<T> ListNode;//重命名结点类型为ListNode
typedef _list_iterator<T, Ref, Ptr> self;//重命名迭代器类型为self
//传const对象和普通对象会分别实例化出下面两种迭代器:
//typedef _list_iterator<T, T&, T*> self;//此时self普通迭代器类型
//typedef _list_iterator<T, const T&, const T*> self;//此时self是const迭代器类型
//构造函数
_list_iterator(ListNode* pnode)
:_pnode(pnode)
{}
//前置++
self& operator++()
{
_pnode = _pnode->_next;//当前迭代器的指针指向下一个结点
return *this;//返回自增后的迭代器
}
//后置++
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;//返回自减前的迭代器
}
//比较两个迭代器是否相同
bool operator==(const self& s) const
{
return _pnode == s._pnode;//直接比较两个迭代器的指针是否一样即可
}
bool operator!=(const self& s) const
{
return _pnode != s._pnode;//直接比较两个迭代器的指针是否不一样即可
}
//
Ref operator*()
{
return _pnode->_val;//返回迭代器的指针指向结点的数据
}
Ptr operator->()
{
return &_pnode->_val; //返回迭代器的指针所指结点的数据的地址
}
//成员变量
ListNode* _pnode; //指向结点的指针
};
//list类
template<class T>
class list
{
typedef ListNode<T> ListNode;//把结点类型重命名为ListNode
typedef _list_iterator<T, T&, T*> iterator;//普通迭代器类型
typedef _list_iterator<T, const T&, const T*> const_iterator;//const迭代器类型
public:
//构造函数
list()
{
_head = new ListNode;//先新开一个结点
//哨兵位自己指向自己
_head->_prev = _head;
_head->_next = _head;
}
//拷贝构造函数
//l2(l1)
list(const list<T>& lt)
{
_head = new ListNode;//申请一个哨兵位
_head->_next = _head;
_head->_prev = _head;
//把lt的数据尾插到当前调用对象的容器
for (const auto& e : lt)
{
push_back(e);//将容器lt当中的数据一个个尾插到新构造的容器后面
}
}
//赋值重载函数
//lt2 = lt;
//list<T>& operator=(const list<T>& lt)
//{
// //防止自己给自己赋值->地址比较
// if (this != <)
// {
// clear();//先清除原来有的内容,但是头结点 不会清除
// //把内容拷贝过去
// for (const auto& e : lt)
// {
// push_back(e);
// }
// }
// return *this;//为了支持连续赋值
//}
//
void swap(list<T>& lt)
{
::swap(_head, lt._head);//直接交换两个容器的哨兵位即可
}
//现代写法
//lt是拷贝构造出来的临时对象
list<T>& operator=(list<T> lt)
{
//两个list的内容直接交换
swap(lt);
return *this;//为了支持连续赋值
}
//迭代器
iterator begin()
{
//返回使用头结点构造出来的普通迭代器
/* iterator tmp = iterator(_head->_next);
return tmp;*/
return iterator(_head->_next);
}
iterator end()
{
//返回使用哨兵位结点构造出来的普通迭代器
return iterator(_head);
}
const_iterator begin() const
{
//返回使用头结点构造出来的const迭代器
return const_iterator(_head->_next);
}
const_iterator end() const
{
//返回使用哨兵位结点构造出来的const迭代器
return const_iterator(_head);
}
T& front()
{
return *begin();//返回第一个数据的引用
}
T& back()
{
return *(--end());//返回尾结点数据的引用
}
//尾插
//没有结点也适用
void push_back(const T& x)
{
//ListNode* newnode = new ListNode(x);//新建一个结点
//ListNode* tail = _head->_prev;//哨兵位的_prev指向的就是尾结点
_head tail newnode 三者链接
//_head->_prev = newnode; //新节点成为新的尾
//tail->_next = newnode;
//newnode->_prev = tail;
//newnode->_next = _head;
//insert:在pos位置前插入
insert(end(), x);//在哨兵位前插入->相当于尾插
}
//头插
//没有结点也适用
void push_front(const T& x)
{
//ListNode* newnode = new ListNode(x);//新建一个结点
//ListNode* phead = _head->_next;//哨兵位的_next指向的就是头结点
_head newnode phead
//_head->_next = newnode;//新节点成为新的头节点
//newnode->_prev = _head;
//newnode->_next = phead;
//phead->_prev = newnode;
insert(begin(),x);
}
//pos迭代器位置前插入
void insert(iterator pos, const T& x)
{
//插入时:pos可以是哨兵位,相当于尾插
//pos是对象,访问成员/成员函数用.访问
assert(pos._pnode);//迭代器的指针不能为空
ListNode* cur = pos._pnode;
ListNode* prev = cur->_prev;
ListNode* newnode = new ListNode(x);
//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());//不能删除哨兵位
ListNode* cur = pos._pnode;
ListNode* prev = cur->_prev;
ListNode* next = cur->_next;
delete cur;//释放cur结点 ->外部的pos迭代器的指针成为野指针
//prev next链接
prev->_next = next;
next->_prev = prev;
return iterator(next); //返回删除位置pos的下一个迭代器
}
//尾删
void pop_back()
{
//end()是哨兵位 --end()就是 _head->_prev 尾结点
erase(--end());
}
//头删
void pop_front()
{
//begin()就是头结点
erase(begin());
}
bool empty() const
{
return begin() == end();
}
//长度
size_t size()
{
//遍历统计
size_t sz = 0;
iterator it = begin();
while (it != end())
{
++sz;
++it;
}
return sz;
}
//清空内容
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
//调整容器的大小为n
void resize(size_t n, const T& val = T())
{
iterator it = begin();//第一个数据的迭代器
size_t len = 0;//记录遍历到的数据个数
while (len < n && it != end())
{
++len;
++it;
}
//跳出循环 -有两种情况:case1:len == n 或者 case2 :已经到达了end()位置
//case1:len == n -> 说明是减少数据个数
if (len == n)
{
//后面位置的数据都删除掉
while (it != end())
{
it = erase(it);
}
}
//case2:len < n ->说明是扩容的情况
else
{
//从len位置开始往后扩容,扩容为n个
while (len < n)
{
push_back(val);
len++;
}
}
}
//析构函数
~list()
{
clear();//先清空原来的内容
delete _head;//释放哨兵位
_head = nullptr;//哨兵位指针置空
}
private:
ListNode* _head; //指向哨兵位的指针
};
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
};
//测试构造+拷贝构造 + 赋值重载
void test_list1()
{
Mango::list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.PrintList();
Mango::list<int> lt2 = lt1;//拷贝构造
lt2.PrintList();
Mango::list<int> lt3;
lt3 = lt1;//赋值重载
lt3.PrintList();
}
//测试正向迭代器的operator* 和operator->
void test_list2()
{
Mango::list<Date> lt;
lt.push_back(Date(2022, 3, 12));
lt.push_back(Date(2022, 3, 13));
lt.push_back(Date(2022, 3, 14));
Mango::list<Date>::iterator it = lt.begin();
while (it != lt.end())
{
//cout << (*it)._year << "/" << (*it)._month << "/" << (*it)._day << endl;
//cout << it->_year << "/" << it->_month << "/" << it->_day << endl;
++it;
}
cout << endl;
}
//测试正向迭代器
void test_list3()
{
Mango::list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
//lt1.clear();
Mango::list<int> lt2(lt1);
for (auto& e : lt2)
{
cout << e << " ";
}
cout << endl;
Mango::list<int> lt3;
lt3.push_back(10);
lt3.push_back(10);
lt3.push_back(10);
lt3.push_back(10);
lt1 = lt3;
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
}
//测试n个值初始化+迭代器区间初始化是否能准确匹配
void test_list4()
{
//没有问题
Mango::list<Date> lt1(5, Date(2022, 3, 15));//用5个日期类对象初始化
for (auto& e : lt1)
{
cout << e._year << "/" << e._month << "/" << e._day << endl;
}
cout << endl;
Mango::list<int> lt2(5, 1);//用5个1初始化 ->但是调用的是迭代器区间初始化->err.报错:非法寻址
for (auto e : lt2)
{
cout << e << " ";
}
cout << endl;
}
//测试构造函数时的列表初始化
void test_list5()
{
Mango::list<int> lt{ 1,2,3,4,5 };
lt.PrintList();
}
//测试反向迭代器
void test_list6()
{
Mango::list<int> lt{ 1,2,3,4,5 };
lt.PrintList();
auto it = lt.rbegin();
while (it != lt.rend())
{
cout << *it << " ";
it++;
}
cout << endl;
}
}
理解类型的力量
反向迭代器
反向迭代器就是对正向迭代器的封装,这样它可以是任意容器的反向迭代器
- 它们的不同就在于++调的是正向迭代器的–;–调的是正向迭代器的++
- 注意:源码中为了使正向迭代器和反向迭代器的开始和结束保持对称,解引用*取的是前一个位置
反向迭代器和正向迭代器的区别就是 ++ – 的方向是相反的,所以反向迭代器封装正向迭代器即可,重载控制++ --的方向
实现版本1:只有一个模板参数(源码版本)
此时List和正向迭代器要增加的内容:
list结构体中增加的:
//list.h增加的
typedef Reverse_iterator<const_iterator> const_reverse_iterator; //const版本的反向迭代器,模板参数const版本的正向迭代器传过去
typedef Reverse_iterator<iterator> reverse_iterator;
//注意:正向迭代器的end()位置就是反向迭代器的rbegin()位置
//正向迭代器的begin()位置就是反向迭代器的rend()位置
reverse_iterator rbegin()
{
return reverse_iterator(end());//end()返回正向迭代器,然后构造一个反向迭代器返回
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
const_reverse_iterator rbegin() const
{
return const_reverse_iterator(end());
}
const_reverse_iterator rend() const
{
return const_reverse_iterator(begin());
}
正向迭代器中新增代码
//正向迭代器
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef ListNode<T> ListNode;//节点类型重命名为ListNode
typedef __list_iterator<T, Ref, Ptr> self;//迭代器类型重命名为self
//新增这两条代码
/*
这里要typedef出来,我们不能取一个类的模板参数,
模板参数不是这个类的成员,我们只能取一个类的成员变量 和 它的内嵌类型
此时reference和pointer就是内嵌类型
*/
typedef Ref reference;
typedef Ptr pointer;
namespace Mango
{
template <class Iterator>
class Reverse_iterator
{
public:
// Iterator是哪个容器的迭代器->传模板参数Iterator过来,reverse_iterator<Iterator>就可以
// 适配出各种容器的反向迭代器,复用的体现
typedef Reverse_iterator<Iterator> self; //self是反向迭代器的类型
/*
Iterator是模板参数,要取它的内嵌类型Ref和Ptr
这里就需要使用typename,凡是要取模板里面的内嵌类型,就需要加typename
告诉编译器后面这个Ref是个类型,让他编译通过,等这个类模板Iterator实例化再去找这个具体的类型
*/
typedef typename Iterator::reference Ref;
typedef typename Iterator::pointer Ptr;
Reverse_iterator(Iterator it)//使用正向迭代器初始化
:_it(it)
{}
//当然也可以写成:
//typename Iterator::reference operator*()
Ref operator*()//如果是普通反向迭代器 : Ref就是T&, 如果是const反向迭代器, Ref就是const T&
{
//复用正向迭代器的--,取得是其前一个位置的数据
Iterator prev = _it;
return *--prev;
}
//typename Iterator::pointer operator->()
Ptr operator->()//如果是普通反向迭代器:Ptr就是T*,如果是const反向迭代器,Ptr就是const T*
{
return &operator*();//返回的是operator*()函数的返回数据的地址
}
//下面的函数的实现和写法1一样
//self是反向迭代器的类型
self& operator++() //前置++
{
--_it; //复用正向迭代器的前置--
return *this;
}
self operator++(int) //后置++
{
self tmp(*this);
_it--; //复用正向迭代器的后置--
return tmp;
}
self& operator--()//前置--
{
++_it;//复用正向迭代器的前置++
return *this;
}
self operator--(int)//后置--
{
self tmp(*this);
++_it;//复用正向迭代器的++
return tmp;
}
bool operator!= (const self& rit) const
{
return rit._it != _it;
}
bool operator== (const self& rit) const
{
return rit._it == _it;
}
private:
Iterator _it;
};
}
但是上述反向迭代器的代码针对vector类型就不适用了,为什么呢?
因为vector的迭代器是原生指针,无法取内嵌类型,那么上面的方式就完蛋了,源码中是通过迭代器萃取技术解决这个问题的
实现方法2:带三个模板参数
为了方便做operator* 和operator->的返回值(为了获取数据类型T),我们还可以加两个类模板参数Ref
、Ptr
,.,但是带了这两个参数更容易理解
list结构体中增加的代码:、
编译器找东西都是就近原则,先在局部找,然后再去全局找!
不管const迭代器先声明还剩普通迭代器先声明,都要指明域
typedef reverse_iterator<const_iterator, const T&, const T*> const_reverse_iterator;
typedef Mango::reverse_iterator<iterator, T&, T*> reverse_iterator;//指定域
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
const_reverse_iterator rbegin() const
{
return const_reverse_iterator(end());
}
const_reverse_iterator rend() const
{
return const_reverse_iterator(begin());
}
正向迭代器中无增加的代码:
反向迭代器的实现:
namespace Mango
{
template <class Iterator, class Ref, class Ptr>
// Iterator是哪个容器的迭代器,reverse_iterator<Iterator>就可以适配哪个容器的反向迭代器(复用)
class reverse_iterator
{
public:
typedef reverse_iterator<Iterator, Ref, Ptr> self;; //self是反向迭代器的类型
reverse_iterator(Iterator it)//构造函数,用一个正向迭代器构造
:_it(it)
{}
Ref operator*() //如果是普通反向迭代器 : Ref就是T&, 如果是const反向迭代器, Ref就是const T&
{
//取的是正向迭代器的前一个位置的数据
Iterator prev = _it;
return *--prev;
}
Ptr operator->()//如果是普通反向迭代器:Ptr就是T*,如果是const反向迭代器,Ptr就是const T*
{
//取的是正向迭代器的前一个位置的数据的地址
Iterator prev = _it;
return &--prev;
//return &operator*();//返回的是operator*()函数的返回数据的地址
}
self& operator++() //前置++
{
--_it; //复用正向迭代器的--
return *this;
}
self operator++(int) //后置++
{
self tmp(*this);
--_it; //复用正向迭代器的--
return tmp;
}
self& operator--()//前置--
{
++_it;
return *this;
}
self operator--(int)//后置--
{
self tmp(*this);
++_it;//复用正向迭代器的++
return tmp;
}
bool operator!= (const self& rit) const
{
return rit._it != _it;//比较两个正向迭代器中是否相同 -> 复用正向迭代器的!=
}
bool operator== (const self& rit) const
{
return rit._it == _it;//比较两个正向迭代器中是否相同 -> 复用正向迭代器的==
}
private:
Iterator _it;//反向迭代器里面的成员是一个正向迭代器
};
为vector增加反向迭代器
- Iterator是哪个容器的迭代器,reverse_iterator就可以适配哪个容器的反向迭代器(复用)*
list和vector的对比:
vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不
同,其主要不同如下:
vector | list | |
---|---|---|
底层结构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
随机访问 | 支持随机访问,访问某个元素效率O(1) | 不支持随机访问,访问某个元素效率O(N) |
插入和删除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空<间,拷贝元素,释放旧空间,导致效率更低 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1) |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭代器失效 | 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 |
使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |
vector
连续的物理空间,既是优势,也是劣势
优势:支持高效的随机访问
劣势:
1.空间不够需要增容,增容代价比较大
2.可能存在一定的空间浪费,会导致频繁的增容,所以一般都会2倍左右增容
3.头部或者中间插入删除需要挪动数据,效率低下
list很好的解决vector的以上问题
1.按需申请释放空间
2.list任意位置支持O(1)插入删除
注意:STL的所有容器都不保证线程安全