list类

1. list的介绍及使用

list和之前容器介绍的一样,第二个参数是内存池,它的底层是一个带头双向循环链表。它的使用很便捷,来看看构造:

有无参的、用n个val、迭代器区间、拷贝构造,发现和之前学过的容器的用法很相似。list的接口中没有resize、reserve这样的说法,因为它不涉及扩容了,不用提前开空间。list里面也提供了一些新的接口:

下面来简单使用一下list:

这里遍历不能用下标加方括号,因为不是连续的空间了,所以迭代器才是通用玩法。再看看insert,从string以后insert都变了,string部分是以下标去插入,现在不是下标,都给的是迭代器:

但它和vector还是有些差别的,比如想从第5个位置插入数据,如果是vector应该是这样写:v.insert(v.begin()+5,10),如果是list不能lt.insert(it.begin()+5,10)这样写。因为vector是连续的物理空间,可以通过加加到第5个位置,但它插入时的代价大,需要把数据都挪走;list物理空间不是连续的,虽然要想实现也可以支持,但代价太大了。list想要再第5个位置插入值可以这样写:

再如list想从3前面插入30,但不知道3在哪个位置,所以可以想到用find,但list没提供find,因为stl有两大组件,容器和算法,外部通常不能访问容器数据,因为是私有的,stl就通过迭代器把它们联合起来,这样公共的一些算法可以提取出来。迭代器没有暴露容器底部细节:

通过统一的方式访问容器,不需要关注底层实现。所以迭代器是优秀的设计,还有个细节是不是it<end而是!=,因为对不连续的空间没法用小于参考。因此算法写了通用的可以用算法中的:

可以在给的迭代器区间查val,算法中只要满足迭代器不管底层怎样都是支持的,而且迭代器设计是左闭右开,找不到返回last。下面体验一下:

那链表的insert有没有迭代器失效问题?没有,因为链表没有扩容野指针问题,也没有挪数据位置意义改变的问题。那erase有吗?

erase可以删除某个位置,删除一段区间。erase肯定有迭代器失效的问题,因为结点都没有了。那如果erase要持续的删除呢?通过接收返回值,返回值指向被删除元素的下一个位置(比如删偶数):

swap就是两个链表直接换,clear就是把结点都清了。链表的相关操作中单独针对性的提供了一些操作,有reverse,但这个接口库中也有,感觉实现了没有意义,反正逻辑实现是一样的:

再来看看sort,库中也有sort,用一下:

但编译的时候发现sort编译报错了:

跳转过去发现底层是ULast-UFirst,底层是快排,链表没法适应该场景。再单独看不同的算法,如上图的sort和reverse,发现模板参数名那有些差异。为什么呢?其实迭代器从功能角度讲是会分类的,分为单向、双向、随机迭代器。单向只能++,双向可以++/--,随机是可以++/--/+/-。如单链表是单向迭代器,双向链表是双向迭代器,vector、string是随机迭代器:

也意味着参数名字暗示了你适合用哪一个算法,如Bid适合用双向,Input适合用单向,Rand适合用随机。之前我们说的正向、反向迭代器是从遍历功能的角度来说,这里是从性质划分,和容器的底层结构有关。因为sort是随机迭代器,所以list调用的时候报错了。因此能否用一个算法要看容器的迭代器到底是哪一种,那我怎么知道是哪一种?文档中其实有说明迭代器类型是哪一种:

如list的迭代器是双向迭代器。它这不是完全按名字匹配的,(随机迭代器可以认为是一个特殊的双向迭代器)随机迭代器满足双向迭代器的性质,所以看到算法中的迭代器是InPut说明单向、双向、随机迭代器都可以用;看到Bid说明双向、随机迭代器可以用:

是上图这样的一种使用关系。所以链表这里写的sort算法是有意义的,但也可以认为除了方便外意义不是很大,因为实际中其实不用list排序,vector效率远高于list:

首先弄个伪随机数方便生成随机数,定义了vector(提前开好空间)和list,然后再两容器中插入相同的数据,对比它们的排序差异,vector用算法sort,链表用自己的sort,可以看到差距很大,list慢一些。再换一种方式,比如排list的数据,现在把list拷贝到vector,然后用vector排序,排完再拷贝回list:

发现list还是慢一些,所以从效率角度看list的sort意义不大。下面继续看,merge就是两个链表可直接进行归并,归并排序的前提是有序,先sort再merge:

unique是唯一的意思,它的本质是去重,去重的前提也是要排序:

因为不排序去重效率非常低。remove就是find+erase,remove这如果这个值不存在:

发现什么事都不干。还有splice,它是结合的意思,功能是把一个链表的内容转移到另外一个(把A链表的结点取下来直接接入到B链表):

接口(1)是把x链表转移到当前链表这个迭代器位置之前;(2)是把x链表的i位置的值转移到当前链表这个位置之前;(3)是把x链表的一部分转移到当前链表这个位置之前。

2. list的模拟实现

下面来进行list的模拟实现,实现前先看一下源代码,这里就大概看一下:

这是链表结点结构,可以看出是一个双向链表(奇怪的是这是void*,不是结点类型指针)。下面看看链表的成员变量:

库中有个特点是很喜欢typedef,找不到可以右击速览定义(转到定义:跳到定义的地方):

可以看到它是一个list_node*,list_node也被typedef过。假设现在不知道list是带头双向循环链表:首先看到有个结点,然后看构造:

无参的构造函数初始化了一个结点,结点的next指向自己,prev指向自己,再速览一下get_node:

看到结果是空间配置器来的,释放结点调的是put_node.头插尾插调的是inster:

现在上手,首先定义一个list_node,这里用struct定义,如果用class里面要用public(因为结点不弄成公有一会很麻烦,除非弄各种友元)。在前面试用链表时我们也没有触碰到结点,所以结点公有时对别人也是隐藏的。下面实现一下:

在定义链表时,先typedef一下,因为类模板中类名不是类型,防止写错,然后写好成员变量:

下面快速写一个出现先用一用,先写个构造,new个结点,prev和next指向自己:

下面再来写push_back,该结构中找到尾就可以插入,tail是head的前一个,然后建立结点连接就可以了:

然后再补充一个构造函数:

这样一个浅浅的链表就出来了。下面就来搭建迭代器:

能不能像之前实现一样,直接Node*充当迭代器?不可以:

因为未来begin给it,it解引用不能得到想要的数据,它解引用后是个结点,并且it++后也不能到下一个位置,因为它不连续。此时运算符重载就要起作用了,再上手一个自定义类型来配合。因为用的时候是这样用:

根据用法结合来写。iterator是类里面typedef的,那迭代器是如何构造出来的?迭代器是一个自定义类型,这个自定义类型成员是一个结点的指针:

我本来也想用结点指针做迭代器,但无奈的是,不能像vector原生指针那样,因为底层结构有差异,但结构指针确实可帮我找到下一个,也可帮我取数据,内置类型不符合我们行为。因此C++可以用类去封装内置类型,然后重载运算符,此时就可以控制它的行为了。外部*it就是调用operator*,++就是调用operator++,运算符重载就变成我们来控制了,这样实现后用起来和之前感觉一样,但实际底层不一样。现在来完成,先实现begin和end:

(1.单参数构造函数支持隐式类型转换 2.返回对象)上图两种本质是一样的,都生成了匿名对象。然后实现解引用,*it去调operator*期望返回值,因为出了作用域结点还在,用引用返回可读可写:

再重载一个++,迭代器++返回的还是迭代器,然后走到node的next就可以:

再重载一个!=,用结点指针来进行相关比较:

下面测试一下:

发现!=有报错,因为end那里是传值返回,返回head的拷贝,是临时对象,具有常性,所以加const就没有问题了:

有了迭代器后范围for也就支持了,库也是直接替换:

看一下下图:

这里还发生了一件事情,这里调begin,通过拷贝构造把值给it,迭代器里目前没有写拷贝构造,默认生成的拷贝构造对内置类型完成浅拷贝,一个指针在这浅拷贝没啥问题。但多个对象指向同一结点为啥没有因多次析构崩溃?因为这个结点不属于迭代器,只是借助结点访问,不管释放什么的,链表里会释放。下面再看后置++,返回++之前的值;还有==,一起补充上:

这样迭代器就基本上完善了。下一个话题是如何设计const迭代器,可以这样设计const迭代器吗?

不可以,迭代器模拟的是指针的行为,指针有两种const指针,一种是const T* ptr1,一种是T* const ptr2,const迭代器是模拟ptr1的行为。如果按照上图那样设计模拟的是ptr2,因为这样设计是迭代器本身不能被修改,也就不可以++修改,我们的目的是让指向的内容不能修改。那如何控制指向数据不能修改?控制返回类型就可以了:

此时有(*it) += 1就会跑不过,因为解引用之后返回的是const对象。按一般的思路我们把类再拷贝一份,这个新类几乎所有的功能和原来是一样的,唯独有些返回值不一样,把__list_iterator的地方改为__list_const_iterator,然后list中再typedef为const_iterator,但这样设计太冗余了,怎么改进?这两个类只是有些接口上返回值不一样,可以通过一个类型去控制返回值就可以。那怎么控制返回值呢?可以去增加模板参数叫Ref:

operator*的返回值是Ref:

从类模板来说Ref是什么并不知道,对普通的iterator它是T引用,对const_iterator来说它是const T引用:

像下图:

返回这都用的类模板,加了模板参数后它们都要跟着改,所以typedef一下,避免又加模板参数后跟着都改:

有些地方喜欢定义成self,然后把__list_iterator<T>改为self。

上图看到,源码中还重载了一个->运算符,并且是三个模板参数。operator->的返回值是pointer,pointer就是模板参数,结果返回数据取地址。operator->有一点我们是可以理解的:指针除了有*解引用还有->解引用,->相比*而言,*是取指针指向的数据,如果是结构体指针就要用->,迭代器模拟指针的行为,什么时候模拟结构体指针呢?

默认返回T*。现在比如有A这样的类:

然后构造一些对象进去:

现在想对自定义类型进行访问:

给*it无法跑,因为*it是取里面的数据,里面的数据是A,A是自定义类型,自定义类型想走也可以,在A中重载流插入。那有没有其它方式可以访问?

可以像上图这样。但这里迭代器模拟的是一个指向结构的指针,要用箭头,所以可以这样写:

但这里有些古怪的地方:(*it)._a1中*it先转化为it.operator*(),返回的是一个A&,然后对象._a1是没啥问题的;it->a1中it->转化为it.operator->(),它的返回值是A*,A*直接跟_a1访问不了,这严格来说正确写法应该是it->->_a1才符合语法,因为运算符重载要求可读性,这可以认为编译器特殊处理省略了一个->。如果是const迭代器,operator->这应该是返回const T*,所以还是多加一个模板参数Ptr:

普通迭代器传T*,const迭代器传const T*,用Ptr代替operator->的T*(==和!=不修改成员函数+const):

链表的核心部分结束,下面完善一下链表:首先看insert,这里不用对pos检查,因为没有说不允许在哪个位置之前插入(反而erase需要检查pos!=end() )。定义cur为pos.node(要插入的结点)(也可以理解为啥迭代器设置为struct,因为全公有,私有还要友元一下),再找前一个位置,再弄一个新节点,然后改变三结点的链接:

删除也是找当前位置的结点,找到前一个与后一个,改变连接关系,删除结点。这里有迭代器失效的问题,因为pos指向的结点被我们释放了,所以返回下一个位置的迭代器(insert在库中返回的是新插入元素的位置):

尾插就是在哨兵位的前面插入,头插就是在第一个位置前面插入:

尾删就是删除end的前一个,顺便实现一下--,头删删除begin位置就可以:

还有个size,遍历计数:

但是如果比喜欢O(N)遍历,可以增加一个size成员,这样就直接return size,前面每次更新一下。clear要清除数据,但不清哨兵位(erase后不能++,所以接收返回值)(若用记录size的方法记得清零):

析构就是先clear,在deleete哨兵位:

现在还剩拷贝构造,拷贝构造也面临深浅拷贝的问题,先拷贝试一下:

运行崩了,原因肯定是析构两次。调式看到报错的地方在erase,实际不一定在这里,调试窗口有个调用堆栈,可看到调用的函数:

main函数调了Test_list3,list3调析构,析构调clear,clear调erase然后崩了。现在完成拷贝构造,首先把初始化做了(比如lt1拷贝构造lt2,lt2的哨兵位头结点好得要出来),然后遍历调push_back:

这里还可以优化一下,构造和拷贝构造初始化那里有些重复,所以有时候会写个empty_init,然后重复的地方复用就行:

下面写赋值,可用传统写法,这里用现代写法来写,拷贝构造出我想要的,然后一交换,顺便完善交换函数:

下面简单测试一下:

还有一个点,库里的赋值和我们写的有些不一样:

我们返回写的是类型,它写的是类名,对拷贝构造和赋值那块写类名也可以,语法上可识别,但不推荐这样写,降低了可读性。可以认为这里是特例,类模板在类里面写类名和类型都是可以的。

3.相关代码

复制代码
//list.h


#pragma once

namespace yxx
{
	template<class T>
	struct list_node
	{
		list_node(const T& val = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_val(val)
		{}

		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;
	};

	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* node)
			:_node(node)
		{}

		Ref& operator*()
		{
			return _node->_val;
		}

		Ptr operator->()
		{
			return &_node->_val;
		}

		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		self& operator--()
		{
			_node = _node->_perv;
			return *this;
		}

		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		bool operator!=(const self& it)const
		{
			return _node != it._node;
		}

		bool operator==(const self& it)const
		{
			return _node == it._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 __list_iterator<T> const_iterator  error

		iterator begin()
		{
			//return _head->next
			return iterator(_head->_next);
		}

		iterator end()
		{
			return _head;
			//return iterator(_head);
		}

		const_iterator begin() const
		{
			//return _head->_next;
			return const_iterator(_head->_next);
		}

		const_iterator end() const
		{
			return _head;
			//return const_iterator(_head);
		}

		void empty_init()
		{
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
			_size = 0;
		}


		list()
		{
			empty_init();
			/*_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
			_size = 0;*/
		}


		//void push_back(const T& x )
		//{
		//	Node* tail = _head->_prev;
		//	Node* newnode = new Node(x);
		//	tail->_next = newnode;
		//	newnode->_prev = tail;
		//	newnode->_next = _head;
		//	_head->_prev = newnode;
		//}

		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;
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}


		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
			_size = 0;
		}

		list(const list<T>& lt)
		{
			/*_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
			_size = 0;*/

			empty_init();

			for (auto& e : lt)
			{
				push_back(e);
			}
		}

		void push_back(const T& x)
		{
			insert(end(), x);
		}

		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);
			prev->_next = newnode;
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
			++_size;
			return newnode;
		}

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

		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;
			--_size;
			return next;
		}

		size_t size()
		{
			return _size;
		}

	/*	size_t size()
		{
			size_t sz = 0;
			iterator it = begin();
			while (it != end())
			{
				++sz;
				++it;
			}
			return sz;
		}*/

	private:
		Node* _head;
		size_t _size;
	};

	class A
	{
	public:
		A(int a1 = 0, int a2 = 0)
			:_a1(a1)
			,_a2(a2)
		{}
		int _a1;
		int _a2;
	};

	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;
	}


	void Test_list2()
	{
		list<A> lt;
		lt.push_back(A(1, 1));
		lt.push_back(A(2, 2));
		lt.push_back(A(3, 3));
		lt.push_back(A(4, 4));

		list<A>::iterator it = lt.begin();
		while (it != lt.end())
		{
		/*	cout << (*it)._a1 << " "; cout << (*it)._a2 << " ";*/
			cout << it->_a1 << " " << it->_a2 << endl;
			++it;
		}
		cout << endl;
	}

	void Test_list3()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		list<int> lt1(lt);
		for (auto e : lt1)
		{
			cout << e << " ";
		}
	}

	void Test_list4()
	{
		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;
		list<int> lt1;
		lt1.push_back(5);
		lt1.push_back(6);
		lt1.push_back(7);
		lt1.push_back(8);
		for (auto e : lt1)
		{
			cout << e << " ";
		}
		cout << endl;
		lt1 = lt;
		for (auto e : lt1)
		{
			cout << e << " ";
		}
	}
}

//Test.cpp

#define _CRT_SECURE_NO_WARNINGS 1

#include <iostream>
#include <stdbool.h>
#include <assert.h>
using namespace std;


#include "list.h"



int main()
{
	yxx::Test_list4();
	return 0;
}
相关推荐
Y40900115 小时前
List、ArrayList 与顺序表
数据结构·笔记·list
jllllyuz2 天前
C++ 中 initializer_list&& 类型推导
开发语言·c++·list
czhc11400756633 天前
linux81 shell通配符:[list],‘‘ ``““
数据结构·chrome·list
Jinkxs3 天前
基础14-Java集合框架:掌握List、Set和Map的使用
java·list
呆瑜nuage4 天前
list的使用和模拟
c++·list
一只余弦函数6 天前
《C++》STL--list容器详解
开发语言·c++·list
死也不注释7 天前
【在Unity游戏开发中Dictionary、List介绍】
数据结构·游戏·unity·游戏引擎·list·游戏程序
geovindu7 天前
ArKTS:List 数组
数据结构·list·harmonyos
cccyi77 天前
c++-list
c++·list