目录
一、前言
本文将围绕C++ STL的list容器展开介绍,list底层以数据结构的带头双向循环链表为结构基础,相比STL中的其他容器,如string、vector,在接口用法上大体类似,其接口也是在其底层结构基础上进行一系列的增删改查等操作,与string、vector有所不同的是由于string、vector在底层结构上内存地址是连续的,而list基于链表为基本结构,结点之间的地址并不是连续的,因此list迭代器的实现方式相比string、vector会有所不同,其实现方式也较为复杂,本文将从list接口的基本用法出发,在此基础上进一步实现list相关接口,其迭代器的实现是list的一大重难点,涉及模板、运算符重载等核心方法,实现要求能力较高,实现起来也较为复杂。
二、list相关接口
1、push_back
cpp
include<iostream>
#include<list>
using namespace std;
void test_list1()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
}
list与vector类似,在STL中实现为模板类型,使用时也需进行实例化,如list<int> lt,实例化了一个结点存放数据类型为int的带头循环双向链表,push_back实现在链表后尾插结点,如lt.push_back(1),从而实现链表数据的尾插。
2、迭代器
cpp
#include<iostream>
#include<list>
using namespace std;
void test_list1()
{
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;
}
list迭代器的用法与vector保持一致,使用迭代器遍历数据时也需进行实例化,如list<int>::iterator it=lt.begin(),begin()指向list第1个有效结点的位置,end()指向list最后一个结点的下一个位置,通过while循环即可实现list的遍历,结果如(1)所示。

(1)
3、范围for
cpp
#include<iostream>
#include<list>
using namespace std;
void test_list1()
{
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;
}
通过范围for也可遍历list,范围for底层为迭代器,本质也是通过迭代器来遍历list,结果如(2)所示。

(2)
4、emplace_back
cpp
void test_list2()
{
list<int> lt;
lt.emplace_back(1);
lt.emplace_back(2);
lt.emplace_back(3);
lt.emplace_back(4);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
emplace_back功能与push_back相似,都可以在链表后尾插数据,如上所示,lt调用emplace接口尾插数据1、2、3、4,结果如(3)所示。

(3)
cpp
struct K
{
public:
K(int k1=1,int k2=1)
:_k1(k1)
,_k2(k2)
{
cout << "K(int k1=1,int k2=1)" << endl;
}
K(const K& kk)
:_k1(kk._k1)
,_k2(kk._k2)
{
cout << "K(const K& kk)" << endl;
}
int _k1;
int _k2;
};
void test_list2()
{
list<K> lt;
K k1(1, 1);
lt.push_back(k1);
lt.push_back(K(2,2));
//lt.push_back(3, 3);不支持构造参数直接构造
lt.emplace_back(k1);
lt.emplace_back(K(2, 2));
lt.emplace_back(3, 3);//支持构造参数直接构造
}
与push_back不同的是,push_back是将已构造的对象添加到容器末尾,并不接受通过传构造参数直接构造,而emplace_back支持通过构造参数直接构造,如lt.emplace_back(3,3),emplace可以通过构造参数(3,3)直接构造对象K,通常情况下,两者性能差异并不大,当需要通过构造参数直接构造对象时,考虑用emplace_back,emplace_back相比push_back能够减少不必要的拷贝。
5、insert
cpp
void test_list3()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
//lt.insert(lt.begin() + 3, 30);list迭代器不支持+、-操作
auto it = lt.begin();
int k = 3;
while (k--)
{
it++;
}
lt.insert(it, 30);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
insert实现在list指定位置插入数据,与string、vector有所不同的是,由于string、vector内存地址是连续的,因此string、vector迭代器支持+、-操作,而list各个结点的地址并不是连续的,因此list迭代器不支持+、-操作,仅支持++、--,若想在list某个位置插入数据,就只能通过迭代器的++、--来实现,例如在list的第3个结点后插入一个数据30,可以通过while循环、迭代器的++来实现,结果为1,2,3,30,4,5,6,如(4)所示。

(4)
6、erase
cpp
void test_list3()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
auto it = lt.begin();
lt.erase(it);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
erase实现在list指定位置删除元素,如auto it=lt.begin(),lt.erase(it),删除list的首元素,结果应为2,3,4,5,6,如(5)所示。

(5)
7、sort
cpp
void test_list4()
{
list<int> lt;
lt.push_back(1);
lt.push_back(3);
lt.push_back(2);
lt.push_back(5);
lt.push_back(4);
lt.push_back(6);
lt.sort();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
sort实现对list数据的排序,lt.sort()默认进行升序排序,故结果为1,2,3,4,5,6,如(6)所示。

(6)
若想进行降序排序,可传greater<int>对象,就可进行降序排序。
cpp
void test_list4()
{
list<int> lt;
lt.push_back(1);
lt.push_back(3);
lt.push_back(2);
lt.push_back(5);
lt.push_back(4);
lt.push_back(6);
lt.sort(greater<int>());
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
lt.sort(greater<int>()),这里通过传greater<int>的匿名对象来实现对lt的降序排序,结果为6,5,4,3,2,1,如(7)所示。

(7)
8、reverse
cpp
void test_list4()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
lt.reverse();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
reverse实现list数据的逆置,lt.reverse(),逆置后结果为6,5,4,3,2,1,如(8)所示。

(8)
9、merge
cpp
void test_list5()
{
std::list<double> lt1, lt2;
lt1.push_back(5.5);
lt1.push_back(2.2);
lt1.push_back(3.3);
lt2.push_back(4.4);
lt2.push_back(1.1);
lt2.push_back(6.6);
lt1.sort();
lt2.sort();
lt1.merge(lt2);
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
for (auto e : lt2)
{
cout << e << " ";
}
cout << endl;
}
merge实现两个list的合并,将一个链表尾插到另一个链表后,形成一个新的链表,lt1.sort(),lt2.sort(),先对lt1、lt2进行升序排序,lt1.merge(lt2),再将lt2尾插到lt1后,lt1数据个数为两链表的数据个数之和,结果为1.1,2.2,3.3,4.4,5.5,6.6,lt2的数据个数为0,如(9)所示。

(9)
10、unique
cpp
void test_list6()
{
list<int> lt;
lt.push_back(1);
lt.push_back(20);
lt.push_back(3);
lt.push_back(5);
lt.push_back(5);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
lt.sort();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
lt.unique();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
unique用于list元素的去重,要求list元素原始为有序,先对lt进行排序,调用sort,lt.sort(),再调用unique,对lt元素进行去重,则lt结果为1,3,4,5,6,20,如(10)所示。

(10)
11、splice
cpp
void test_list7()
{
std::list<int> lt1, lt2;
std::list<int>::iterator it;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt2.push_back(10);
lt2.push_back(20);
lt2.push_back(30);
it = lt1.begin();
it++;
lt1.splice(it, lt2);
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
for (auto e : lt2)
{
cout << e << " ";
}
}
splice与merge类似,实现在链表指定位置插入链表,it指向链表第2个元素位置,lt1.splice(it,lt2),在it位置插入lt2,其功能类似剪切,故lt1结果为1,10,20,30,2,3,4,lt2中没有元素,如(11)所示。

(11)
cpp
void test_list7()
{
std::list<int> lt1, lt2;
std::list<int>::iterator it;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
it = lt1.begin();
it++;
lt1.splice(lt1.begin(), lt1, it);
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
}
此外,splice还支持在一条链表内进行操作,it指向链表第2个结点,lt1.splice(lt1.begin(),lt1,it),相当于将链表第2个元素剪切复制到链表的首元素,故lt1结果为2,1,3,4,如(12)所示。

(12)
splice同时也支持区间形式:
cpp
void test_list7()
{
std::list<int> lt1;
std::list<int>::iterator it;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
it = lt1.begin();
it++;
lt1.splice(lt1.begin(), lt1, it, lt1.end());
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
}
lt1.splice(lt1.begin(),lt1,it,lt1.end()),将it位置到lt1.end()的数据剪切拷贝到begin()位置之前,故lt1的结果为2,3,4,1,如(13)所示。

(13)
三、list实现
1、文件
list实现文件包括:list.h头文件,负责list相关接口声明和实现,test.cpp测试文件对list.h实现的接口进行测试,如(14)所示。

(14)
2、结点实现
实现list带头双向循环链表结构,首先需实现链表结点结构,这里都实现为模板类型:
cpp
#pragma once
#include<iostream>
#include<assert.h>
#include<list>
using namespace std;
namespace YZK
{
template<class K>
struct list_node
{
K _data;
list_node<K>* _next;
list_node<K>* _prev;
list_node(const K& data = K())
:_data(data)
, _next(nullptr)
, _prev(nullptr)
{}
};
}
结点list_node中存放数据_data,以及指向下一个结点的指针list_node<K>*_next,指向前一个结点的指针list_node<K>*_prev,其构造函数数据默认初始化为该类型的默认构造K(),_next,_prev初始化为nullptr,从而实现结点的默认构造。
3、list成员变量
cpp
template<class K>
class list
{
typedef list_node<K> Node;
private:
Node* _head;
size_t _size;
};
实现了结点结构,就可以声明list的成员变量了,可先typedef将结点实例化list_node<K>list为Node,方便使用,list为带头双向循环链表,故指向哨兵位头结点的指针可声明为成员变量,_size用于记录list的结点个数。
4、empty_init
cpp
template<class K>
class list
{
typedef list_node<K> Node;
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
private:
Node* _head;
size_t _size;
};
empty_init用于list的初始化,list为带头双向循环链表,故初始化即对头结点进行初始化,先通过new申请一个结点空间,该结点即为头结点,初始化list时,list中没有其他结点,故头结点的_next、_prev指针初始化均指向自身,_size初始化为0,就完成了对list的初始化。
5、构造函数
cpp
list()
{
empty_init();
}
实现list构造函数,就只需调用已实现的empty_init即可完成对list的初始化。
6、size
cpp
size_t size() const
{
return _size;
}
size用于返回list的结点个数,即返回_size。
7、empty
cpp
bool empty() const
{
return _size == 0;
}
empty用于判断list是否没有结点,可通过_size是否为0来判断,即返回_size==0。
8、迭代器模板实现
(1)实现原理
list迭代器实现与string、vector会有所不同,string、vector迭代器可通过原生指针来实现,而list迭代器则不能简单地通过原生指针来实现,这是由于list结点之间地址并不连续,则++it就不一定是下一个结点的地址,此外,*it表示的是it指向的结点,并不是表示该结点的数据,因此需重载*、++等操作符,需要将迭代器进行封装,此外,这里可以利用模板一并实现iterator、const_iterator迭代器,list迭代器的实现是list的一大难点,综合了模板、运算符重载等方法,实现要求能力较高。
(2)构造
cpp
template<class K,class Ref,class Ptr>
struct list_iterator
{
typedef list_node<K> Node;
typedef list_iterator<K,Ref,Ptr> Self;
Node* _PNode;
list_iterator(Node* PNode)
:_PNode(PNode)
{
}
};
list迭代器构造还是通过结点指针来构造,成员变量为结点指针_PNode,typedef结点list_node<K>为Node,list_iterator<K,Ref,Ptr>迭代器为self,方便使用,其构造函数通过传一个结点指针PNode来进行构造。
(3)operator++()
cpp
Self& operator++()
{
_PNode = _PNode->_next;
return *this;
}
operator++()实现前置++,返回指向下一个结点的指针,即_PNode=_PNode->_next,return *this,非局部变量返回引用可减少拷贝。
(4)operator--()
cpp
Self& operator--()
{
_PNode = _PNode->_prev;
return *this;
}
operator--()实现前置--,返回指向前一个结点的指针,即_PNode=_PNode->_prev,return *this,同理返回引用可减少拷贝。
(5)operator++(int)
cpp
Self operator++(int)
{
Self tmp(*this);
_PNode = _PNode->_next;
return tmp;
}
operator++(int)实现后置++,返回原始值,故先构造tmp保存原始值,再进行++操作,即_PNode=_PNode->_next,最后返回tmp即可,由于tmp为局部变量,出作用域就被销毁,故不能返回引用,只能传值返回。
(6)operator--(int)
cpp
Self operator--(int)
{
Self tmp(*this);
_PNode = _PNode->_prev;
return tmp;
}
operator--(int)实现后置--,与operator++(int)类似,同样需先构造tmp保存原始值,再进行--操作,_PNode=_PNode->_prev,最后返回tmp即可,这里也需传值返回,不能引用返回。
(7)operator==
cpp
bool operator==(const Self& s) const
{
return _PNode == s._PNode;
}
operator==用于判断两个迭代器是否相等,可通过其结点指针是否相等来进行判断,即return _PNode==s._PNode。
(8)operator!=
cpp
bool operator!=(const Self& s) const
{
return _PNode != s._PNode;
}
operator!=则判断两个迭代器不相等,与operator==类似,可通过其结点指针判断,即return _PNode!=s._PNode。
(9)operator*
cpp
template<class K,class Ref,class Ptr>
struct list_iterator
{
typedef list_node<K> Node;
typedef list_iterator<K,Ref,Ptr> Self;
Node* _PNode;
list_iterator(Node* PNode)
:_PNode(PNode)
{
}
Ref operator*()
{
return _PNode->_data;
}
operator*重载用于访问当前结点的数据,即return _PNode->_data,这里返回类型为模板参数Ref,这是考虑到普通迭代器和const修饰迭代器二者返回类型的差异,对于普通迭代器,返回类型为K&,而对于const修饰的迭代器返回类型为const K&,故将返回类型作为模板参数Ref,可根据具体迭代器设置Ref的值,从而做到一个迭代器模板可实现两个不同类型的迭代器,简化了迭代器不必要的重复实现,这也是list迭代器实现的一个核心思想和难点所在。
(10)operator->
cpp
template<class K,class Ref,class Ptr>
struct list_iterator
{
typedef list_node<K> Node;
typedef list_iterator<K,Ref,Ptr> Self;
Node* _PNode;
list_iterator(Node* PNode)
:_PNode(PNode)
{
}
Ref operator*()
{
return _PNode->_data;
}
Ptr operator->()
{
return &_PNode->_data;
}
}
operator->重载用于访问结点元素的地址,即结点元素的指针,即return &_PNode->_data,返回类型为模板参数ptr,也是基于普通迭代器和const修饰迭代器返回类型的不同,普通迭代器返回类型为K*,而const修饰的迭代器返回类型为const K*,故模板参数Ptr可根据具体迭代器来设置,从而做到一个迭代器模板既能实现普通迭代器,也能实现const修饰的迭代器,从而简化了两个迭代器实现不必要的重复。
9、迭代器相关接口实现
(1)迭代器实现
cpp
template<class K>
class list
{
typedef list_node<K> Node;
public:
typedef list_iterator<K, K&, K*> iterator;
typedef list_iterator<K, const K&, const K*> const_iterator;
private:
Node* _head;
size_t _size;
};
实现了迭代器模板,就可以借助模板实现迭代器了,只需传相应的模板参数即可,可知list_iterator<K,K&,K*>即为普通迭代器,typedef为iterator,list_iterator<K,const K&,const K*>为const修饰的迭代器,typedef为const_iterator,这样就借助迭代器模板实现了iterator和const_iterator两个不同类型的迭代器。
(2)begin、end
cpp
template<class K>
class list
{
typedef list_node<K> Node;
public:
typedef list_iterator<K, K&, K*> iterator;
typedef list_iterator<K, const K&, const K*> const_iterator;
iterator begin()
{
return _head->_next;
}
iterator end()
{
return _head;
}
const_iterator begin() const
{
return _head->_next;
}
const_iterator end() const
{
return _head;
}
private:
Node* _head;
size_t _size;
};
begin用于返回list第1个有效结点的迭代器,即头结点的下一个结点的迭代器,即return _head->_next,_head->_next会根据迭代器的具体类型隐式类型转化为相应的迭代器类型,转化为iterator或者const_iterator。end返回list最后一个结点下一个位置的迭代器,由于list为带头双向循环链表,可知最后一个结点下一个位置的迭代器即为头结点的迭代器,即return _head,同理_head会隐式类型转化为相应的迭代器类型,iterator或const_iterator。
10、insert
insert实现在list指定位置pos之前插入一个新结点,list中插入新结点只需处理结点之间_prev、_next指针的连接问题,例如在链表1、2、4中在4之前插入一个新结点3,则需处理2与3,3与4结点的指针连接问题,如下图所示:

需改变2和4结点_prev、_next指针的指向,以及新结点3_prev、_next指针的指向,2的_next指针指向新结点,3的_prev指针指向2,4的_prev指针指向3,3的_next指针指向4,从而完成新结点的插入。
cpp
iterator insert(iterator pos, const K& x)
{
Node* cur = pos._PNode;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
++_size;
return newnode;
}
insert在pos位置之前插入新结点,pos._PNode获取该位置的结点指针cur,cur->_prev为pos位置的前一个结点指针prev,再通过new开辟并初始化新结点newnode,剩下的就是处理prev、newnode、cur三者指针的指向问题,与上面类似,prev的_next指针指向newnode,newnode的_prev指针指向prev,newnode的_next指向cur,cur的_prev指针指向newnode,++_size,最后返回该位置的迭代器,即新结点位置的迭代器newnode。
这里需要注意的是,list的insert并不会导致迭代器失效,因为list的insert只需处理结点之间的_next、_prev指针指向问题,并没有发生扩容,因此结点的迭代器并没有发生改变,迭代器不失效。
11、push_back
cpp
void push_back(const K& x)
{
insert(end(), x);
}
实现了insert,就可以借助insert来实现push_back,push_back实现list的尾插,即在end位置前插入新结点,即insert(end(),x),就实现了push_back。
12、print_container
cpp
template<class container>
void print_container(const container& con)
{
typename container::const_iterator it = con.begin();
while (it != con.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
print_container实现为函数模板,用于遍历输出容器数据,通过迭代器、while循环即可遍历输出容器数据,需要注意的是未实例化的类container,引用其成员const_iterator迭代器时,需加上typename关键字,否则编译器无法区分是类型还是成员变量。
测试(test.cpp)
cpp
void test_list1()
{
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;
print_container(lt);
}
对上面实现的迭代器、push_back、print_container进行测试,输出结果为1,2,3,4,如(15)所示,结果正确,测试通过。

(15)
13、operator->测试
cpp
struct kk
{
int _k1 = 1;
int _k2 = 1;
};
void test_list1()
{
list<kk> ltk;
ltk.push_back(kk());
ltk.push_back(kk());
ltk.push_back(kk());
ltk.push_back(kk());
list<kk>::iterator itk = ltk.begin();
while (itk != ltk.end())
{
//cout<<(*itk)._k1<<":"<<(*itk)._k2<<endl;
cout << itk->_k1 << ":" << itk->_k2 << endl;
//cout << itk.operator->()->_k1 << ":" << itk.operator->()->_k2 << endl;
++itk;
}
cout << endl;
}
operator->重载比较少用,主要使用在list结点元素为结构体时,这时operator->获取的就是该结点结构体元素的指针,如上所示,ltk结点元素为结构体kk,这时operator->获取的就是结构体kk的地址,这时结构体kk的指针再进一步使用->操作符,就可访问kk的数据,具体表示为itk.operator->()->_k1,itk.operator->()->_k2,这样写起来比较麻烦,也不美观,对此编译器进行了优化,省略了一个->,直接表示为itk->_k1,itk->_k2,此外,除了用operator->表示,也可用operator*重载表示,即(*itk)._k1,(*itk)._k2,*itk即表示结构体kk,再使用.操作符就可访问_k1、_k2,结果应为1:1,如(16)所示,结果正确,测试通过。

(16)
14、push_front
cpp
void push_front(const K& x)
{
insert(begin(), x);
}
push_front实现list的头插,可借助insert来实现,即在begin位置之前插入一个新结点,即insert(begin(),x),就实现了push_front。
15、erase
erase用于删除list指定位置的元素,与insert一致,erase也需处理结点之间的_prev、_next指针的指向问题,可参考下图所示:

删除pos位置的结点3,需处理结点2、结点4的_prev、_next的指针问题,结点2的_next指针指向4,结点4的_prev指针指向2,同时也要释放结点3的空间,就完成了list结点的删除。
cpp
iterator erase(iterator pos)
{
assert(pos != end());
Node* prev = pos._PNode->_prev;
Node* next = pos._PNode->_next;
prev->_next = next;
next->_prev = prev;
delete pos._PNode;
--_size;
return next;
}
erase删除指定位置元素,end为list头结点位置的迭代器,因此指定位置迭代器pos不能为end,接下来就只需获取pos前一个结点指针和后一个结点指针,即pos._PNode->_prev为pos的前一个结点指针,pos._PNode->_next为pos的后一个结点指针,与上面类似,再将prev的_next指针指向next,next的_prev指针指向prev,最后通过delete释放该结点,--_size,返回pos下一个结点的迭代器next,就实现了erase。
erase与insert不同的是,erase会导致迭代器的失效,这是因为erase释放了结点空间,使得该结点的迭代器变为野指针而失效,其他结点的迭代器并不失效,pos位置的迭代器失效,因此erase后需更新迭代器的值才能访问。
测试(test.cpp)
cpp
void test_list2()
{
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();
lt.insert(it, 10);
*it += 100;
print_container(lt);
it = lt.begin();
while (it != lt.end())
{
if (*it % 2 == 0)
{
it=lt.erase(it);
}
else
{
++it;
}
}
print_container(lt);
}
对insert、erase进行测试,lt.insert(it,10),lt头插10,*it+=100,可知结果为10,101,2,3,4,接着通过while循环删除偶数,erase需更新迭代器的值,即it=lt.erase(it),可知结果101,3,如(17)所示,结果正确,测试通过。

(17)
16、pop_back
cpp
void pop_back()
{
erase(--end());
}
pop_back实现list结点的尾删,可借助erase来实现,即删除end的前一个位置的结点,即erase(--end()),就实现了pop_back。
17、pop_front
cpp
void pop_front()
{
erase(begin());
}
pop_front实现list结点的头删,也可借助erase来实现,即删除begin迭代器位置的结点,即erase(begin()),就实现了pop_front。
18、clear
cpp
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
clear用于清空list的结点,可通过erase、while循环来实现结点的删除,需要注意的是erase删除时需更新迭代器的值,才能继续访问。
19、析构
cpp
~list()
{
clear();
delete _head;
_head = nullptr;
}
list的析构实现可先调用clear,清空list的结点,再释放头结点空间,最后将头结点置为nullptr,就完成了list的析构。
20、拷贝构造
cpp
list(const list<K>& lt)
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
实现list的拷贝构造,先调用empty_init完成头结点的初始化,再通过范围for将lt的数据逐个尾插,就实现了list的拷贝构造。
21、swap
cpp
void swap(list<K>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
swap函数用于两个list对象的交换,可通过调用std库的swap函数交换两个list的_head,_size,这样就实现了两个list的交换。
22、operator=
cpp
list<K>& operator=(list<K> lt)
{
swap(lt);
return *this;
}
operator=重载用于list的赋值,可借助swap函数实现,传赋值对象的拷贝lt,再通过swap交换lt与被赋值对象,这样被赋值对象就完成了赋值,lt出作用域会自动被析构,返回*this即可,引用返回可减少拷贝,这样就通过swap间接实现了operator=。
测试(test.cpp)
cpp
void test_list3()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
list<int> lt2(lt1);
print_container(lt2);
list<int> lt3;
lt3.push_back(10);
lt3.push_back(20);
lt3.push_back(30);
lt3.push_back(40);
lt1 = lt3;
print_container(lt1);
}
对拷贝构造、operator=进行测试,将lt1拷贝构造给lt2,则lt2结果为1,2,3,4,将lt3赋值给lt1,则lt1结果为10,20,30,40,如(18)所示,结果正确,测试通过。

(18)
23、初始化列表
除了通过构造、拷贝构造来初始化list之外,C++11还引入了初始化列表来初始化list,即initializer_list,用{ }表示,{ }用于存放数据,例如{1,2,3,4,5,6},实现方式与拷贝构造实现类似。
cpp
list(initializer_list<K> il)
{
empty_init();
for (auto& e : il)
{
push_back(e);
}
}
即先通过empty_init初始化头结点,再通过范围for将il的数据逐个尾插,就实现了通过初始化列表来初始化list。
测试(test.cpp)
cpp
void test_list4()
{
list<int> lt1({ 1,2,3,4,5,6 });
list<int> lt2 = { 1,2,3,4,5,6,7};
print_container(lt1);
print_container(lt2);
}
对list初始化列表进行测试,lt1是通过传初始化列表{1,2,3,4,5,6}来构造lt1,lt2则是通过隐式类型转换来进行构造,则lt1结果为1,2,3,4,5,6,lt2结果为1,2,3,4,5,6,7,如(19)所示,结果正确,测试通过。

(19)
四、结语
本文围绕STL的list容器展开介绍,list底层以带头双向循环链表为结构基础,其接口用法上与string、vector类似,着重介绍了list的常用接口和用法,以及进一步实现了list的相关接口,需要注意的是,与string、vector不同的是,list的insert不会导致迭代器的失效,这是由于list的结点插入只涉及各结点的prev、next指针,并不涉及指向结点的指针,因此结点的迭代器并没有发生变化,迭代器并不失效,而erase涉及删除结点空间的释放,删除结点的迭代器就为野指针了,因此删除结点的迭代器失效,其他结点的迭代器不失效,删除结点后由于删除结点的迭代器失效,因此需要更新迭代器的值才能进行访问。此外,list迭代器的实现是list实现的一大难点,由于string、vector底层内存地址是连续的,因此可直接借助原生指针实现迭代器,而list基于链表为基本结构,各个结点之间不一定是连续的,因此不能直接通过原生指针来实现,对此需要通过对list迭代器进行封装,以及对operator*、operator->进行重载,从而实现迭代器的功能,通过模板,可以一并实现普通迭代器以及const修饰的迭代器,减少不必要的重复实现,list迭代器的实现也体现了化繁为简、转化等核心思想,这些思想和方法也是学习其他容器的核心方法,从list容器出发,以小见大,可以窥见更为浩瀚的STL世界!