list深度剖析

一、list介绍与使用

1.关于list

list即为链表,而且还是双向带头循环链表,其详细介绍参考C++官网
点我

2.list的使用

这里只介绍一下构造函数,其余函数参考官网

构造函数(constructor) 接口说明
list (size_type n, const value_type& val =value_type()) 构造的list中含n个值为val的元素
list() 构造空的list
list(const list& x) 拷贝构造
list (InputIterator first, InputIterator last) 用[first, last)区间中的元素构造list

3.迭代器相关说明

迭代器类型 ++ - - +n -n 典型容器
输入(Input) 输入流
输出(Output) 输出流
前向(Forward) forward_list
双向(Bidirectional) listsetmap
随机访问(Random) vectordequearray

list采用的是双向迭代器,而vector采用的是随机访问迭代器,因此list_iterator不支持 +/-,也不支持[]下标
我们来看看sort在这里插入图片描述
显而易见,算法库的sort要的参数是随机访问迭代器,因此,list不能使用sort,但是list有它自己实现的sort

二、list相关接口简述

l.empty()      		// 是否为空
l.size()       		// 元素个数
l.max_size()   		// 最大容量(一般不用)
l.front()   		// 第一个元素
l.back()    		// 最后一个元素
l.push_back(10)		//尾插10
l.push_front(20)	//头插20
l.pop_back()		//尾删
l.pop_front()		//头删
  • insert:指定位置插入
  • erase:指定位置删除
  • remove:按值删除
  • removeif:条件删除
  • unique:去重
  • reverse:翻转
  • merge:合并链表,需要两链表有序,类似于归并
  • splice:剪切链表

简要区分:emplace_back与push_back

list<A> lt;
A aa(1,1);
lt.push_back(aa);
lt.push_back(A(2,1));
//lt.push_back(2,2);	这是错误的

lt.emplace_back(aa);
lt.emplace_back(A(2,1));
lt.emplace_back(2,2);	//没有问题

emplace_back和push_back前两种插入效率差不多,但是emplace_back可以进行第三种方式进行插入,且效率更高,因为它只进行了构造,而前两种进行了构造加拷贝

关于splice接口

splice就是把一个list的节点直接移动到另一个list

std::list<int> l1 = {1,2,3};
std::list<int> l2 = {4,5};

auto it = l1.begin();
++it;

l1.splice(it, l2);

结果:

l1: 1 4 5 2 3
l2: 空

vector的insert可能会导致迭代器失效,而splice并不会导致迭代器失效,因为splice只是移动指针,并没有发生扩容导致指针指向位置偏移,并不会导致迭代器失效

简述迭代器失效

链表插入数据后并不会发生迭代器失效,但在删除后可能发生

  • 插入数据后,原来迭代器指向的位置不会发生改变,故不会发生迭代器失效
  • 删除数据后,若删除的是迭代器指向的数据,那么该迭代器失效,其他的迭代器不会产生任何影响

三、源码剖析:根据源码重构list

1.结构分析

要实现list,必须包含三个类,分别是:节点类迭代器类链表类,以下list均用List表示

2.节点类实现

//List节点类
template<class T>
struct ListNode
{
	ListNode<T>* _prev;
	ListNode<T>* _next;
	T _data;
};

list节点类包含指向前驱节点指针_prev指向后继结点的指针_next节点数据_data,节点使用struct进行封装,是因为后面的程序会频繁调用节点的成员

ListNode(const T& data = T())
		:_prev(nullptr),
		_next(nullptr),
		_data(data)
	{ }

这里需要一个节点的构造函数,我们可以将其全部置空,这里的data=T(),对于内置类型也是兼容的

3.迭代器类实现

3.1 成员变量

	//List迭代器类
	template<class T>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator self;
		Node* _pnode;
	};

list的迭代器需要一个封装的指针,因为list的空间不是连续的,原生指针难以实现++,- -,解引用等操作,这些需要我们手动去执行

3.2 构造函数

ListIterator(Node* pnode = nullptr)
	:_pnode(pnode)
{ }

迭代器本身就是一个封装的指针,因此浅拷贝足够,不需要我们去实现

3.3 operator*

T& operator*() { return _pnode->_data; }

迭代器就是一个封装的指针,解引用需要返回指针指向的数据

3.4 operator++和operator- -

self& operator++()
{
	_pnode = _pnode->_next;
	return *this;
}
self operator++(int)
{
	auto tmp = *this;
	_pnode = _pnode->_next;
	return tmp;
}
self& operator--()
{
	_pnode = _pnode->_prev;
	return *this;
}
self& operator--(int)
{
	auto tmp = *this;
	_pnode = _pnode->_prev;
	return tmp;
}

这里是前置++、后置++、前置- -、后置- -

3.5 operator==和operator!=

bool operator==(const self& s) const { return s._pnode == _pnode; }
bool operator!=(const self& s) const { return s._pnode != _pnode; }

直接比较指针是否相等

3.6 operator->

T* operator->() { return &(_pnode->_data); }

既然这个迭代器是指针,那就有指针->的方式去访问节点数据
举一个简单的例子:

class A
{
public:
	int _a;
	int _b;
};
List<A> lt;
List<A>::iterator it=lt.begin();
//取it指向节点的_b元素
cout<<it->_b<<endl;

取_b元素实际上是

it.operator->()->_b

4.List实现

4.1 成员变量

//List类
template<class T>
class List
{
public:
	typedef ListNode<T> Node;
private:
	Node* _head;
	size_t _size;
};

List需要一个指向头结点的指针_head和一个 _size便于统计长度

4.2 构造函数

接下来,我们需要一个构造函数,因为后面链表各种构造方式都需要一个空链表,所以我们先实现一个空链表的构造:先构造一个哨兵头结点,将头结点的前驱和后继都指向自己,将_size置零即可

//空构造
void empty_init()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
	_size = 0;
}
//构造
List(){ empty_init(); }
//拷贝构造
list(const list<T>& lt){
	empty_init();
	for (auto& e : lt)
		push_back(e);
}

4.3赋值重载

复制重载可以使用swap的方式完成,这是一种经典且高效的写法,它避免了一个个拷贝的时间消耗,而是在自己的swap中调用库里的swap

void swap(List<T>& lt)
{
	std::swap(_head, lt._head);
	std::swap(_size, lt._size);
}
List<T>& operator=(List<T> lt)
{
	swap(lt);
	return *this;
}

4.4 析构函数

先写一个clear函数清除节点,再去实现析构,注意:析构要删除头结点,clear不用

void clear()
{
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
	}
	_size=0;
}
~List()
{
	clear();
	delete _head;
	_head = nullptr;
}

4.5 插入删除

插入删除注意要保持双向链表的结构

void insert(iterator pos,const T& val)
{
	Node* cur = pos._pnode;
	Node* prev = cur->_prev;
	Node* newnode = new Node(val);
	newnode->_next = cur;
	cur->_prev = newnode;
	newnode->_prev = prev;
	prev->_next = newnode;
	++_size;
}
void erase(iterator pos)
{
	Node* node = pos._pnode;
	if (node == _head) return;
	Node* prev = node->_prev;
	Node* next = node->_next;
	prev->_next = next;
	next->_prev = prev;
	delete node;
	--_size;
}
void push_back(const T& val) { insert(end(), val); }
void pop_back() { if (!empty()) erase(--end()); }
void push_front(const T& val) { insert(begin(), val); }
void pop_front() { if (!empty()) erase(begin()); }

5.const迭代器的实现

要实现const迭代器可以采用复制一份普通迭代器的方法来实现,但是这样代码长度太长了,接下来,我们看一种stl库里实现的方法
看ListIterator类,我们对它的模板参数进行修改,传三个参数,第一个是类型,第二、三个分别是类型的引用和指针将ListIterator<T, T&, T*>定义为iterator,而const ListIterator<T, const T&, const T*>定义为const_iterator,接下来把之后T&改成Ref,T*改成Ptr即可,这样编译器就能自动识别你要的是iterator还是const_iterator了

//List迭代器类
template<class T,class Ref,class Ptr>
struct ListIterator
{
	typedef ListIterator<T, T&, T*> iterator;
	typedef const ListIterator<T, const T&, const T*> const_iterator;
	typedef ListNode<T> Node;
	typedef ListIterator<T, Ref, Ptr> self;
	Node* _pnode;
}

6. 关于initializer_list

stl库支持这样的初始化:

list<int> lt={1,2,3,4,5};

这是C++11新增的初始化方式:初始化列表初始化
在这里插入图片描述
看看initializer_list是怎样的
在这里插入图片描述
看看它的底层:可见,它的底层是_First和_Last两个指针,_First指向第一个元素,_Last指向最后一个元素的下一个位置
在这里插入图片描述
因此,我们可以写一个用initializer_list初始化的构造函数

List(std::initializer_list<T> il)
{
	empty_init();
	for (auto i : il)
	{
		push_back(i);
	}
}
List<int> lt({1,2,3,4});	//lt: 1->2->3->4

更多推荐