从基本用法到迭代器实现—list重难点突破

目录

一、前言

二、list相关接口

1、push_back

2、迭代器

3、范围for

4、emplace_back

5、insert

6、erase

7、sort

8、reverse

9、merge

10、unique

11、splice

三、list实现

1、文件

2、结点实现

3、list成员变量

4、empty_init

5、构造函数

6、size

7、empty

8、迭代器模板实现

(1)实现原理

(2)构造

(3)operator++()

(4)operator--()

(5)operator++(int)

(6)operator--(int)

(7)operator==

(8)operator!=

(9)operator*

(10)operator->

9、迭代器相关接口实现

(1)迭代器实现

(2)begin、end

10、insert

11、push_back

12、print_container

13、operator->测试

14、push_front

15、erase

16、pop_back

17、pop_front

18、clear

19、析构

20、拷贝构造

21、swap

22、operator=

23、初始化列表

四、结语


一、前言

本文将围绕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世界!

相关推荐
努力学习的小全全2 小时前
【CCF-CSP】06-01数位之和
c++·ccf-csp
再卷也是菜2 小时前
C++篇(16)C++11(下)
c++
java_logo2 小时前
TOMCAT Docker 容器化部署指南
java·linux·运维·docker·容器·tomcat
CS_浮鱼2 小时前
【C++进阶】智能指针
开发语言·c++
怕什么真理无穷2 小时前
C++_面试题_21_字符串操作
java·开发语言·c++
百***07182 小时前
Node.js 与 Docker 深度整合:轻松部署与管理 Node.js 应用
docker·容器·node.js
令狐少侠20112 小时前
docker启动失败
运维·docker·容器
Dream it possible!3 小时前
LeetCode 面试经典 150_二叉树_二叉树展开为链表(74_114_C++_中等)
c++·leetcode·链表·面试·二叉树
cookies_s_s4 小时前
C++20 协程
linux·开发语言·c++