模拟实现STL容器之list

前言

本文主要介绍对list模拟实现,list模拟实现最精华的地方在于迭代器的实现。通过手动实现list让我们进一步更加深刻认识到模板的作用。本文对数据结构方面的实现可能不多,这里主要围绕C++语法特性进行介绍。

1.大致思路

实现之前我们可以先去看看STL库中的源码对list的实现。早在数据结构链表的博客中就提到过库中的list采用的双向循环链表的结构,所以我们也采用这种方式来实现list。list的实现难点在于迭代器的实现,我们知道链表的节点都是new出来的,这样我们就无法使用原生指针来充当迭代器。其实在源码中,库中实现了一个迭代器模板类,通过这个类来模拟原生指针的行为。这里我们也是采用这种方式来实现list的迭代器。

2.代码具体实现

1.创建节点类和list类

我们知道list是由一个个节点组成的,所以我们先创建一个节点类和一个list类.这点和C语言很不一样,C语言list结构体有节点指针成员就行了,C++在设计的时候这个node单独是一个类,list中的成员是node类型的变量。C++中每个自定义类型都设计出一个对应的类。这样才更加符合面向对象的设计思想,在写代码的逻辑也会清晰。

代码示例

namespace Ly
{  //创建节点模板
	template<class T>
	struct node
	{  //为后面new节点的顺便进行初始化有点像之前写的Buynode
		node(const T& val = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(val)
		{

		}
		node<T>* _next;//地址域
		node<T>* _prev;//地址域
		T _data;//值域
	};

template<class T>
	class list
	{
	public:
	typedef node<T> node;
	 private:
		node* _head;

	};
}

这里的node是用的struct定义的主要原因的是因为为了便于访问,struct的默认权限是public不会受到限制,用class定义也行,把权限设为public即可。关于这个参数给匿名对象的缺省值在之前vectord的博客中也提到过,这里就不再赘述了。这个node给了一个构造函数,这是为了再new节点的时候便于初始化。这里为了便于书写将node< T >重命名成了node。node析构函数就不写了,链表的节点最后由链表自己析构,也就是list类析构。


2.迭代器的设计

1.正向迭代器

学习实现list最精华的点在于迭代器的设计,至于链表的增删查改这都是数据结构那一套没啥好说。这些逻辑实现起来很简单,但是迭代器的实现就需要我们思考一番了。list无法使用原生指针来充当迭代器,所以我们要自行设计代器。我们可以先去查阅stl源码进行学习,这里就直接说了。slt库中是写一个迭代器类模板来模拟原生指针的行为充当迭代器的。也就说在list中除了自定义类型lnode成员外,还有自定义类型iterator。这个iterator类型就是list迭代器的类型。

template<class T,class Ref,class Ptr>
	struct list_iterator
	{
		typedef node<T> node;//重命名类型便于使用
		typedef list_iterator self; //重命名类名便于使用
		node* _node;
		list_iterator( node* new_node)
			:_node(new_node)
		{

		}
		self& operator++()
		{
			_node=_node->_next;
			return *this;
		}
		self operator++(int)
		{
			self tem(_node);
			_node = _node->_next;
			return tem;
		}
		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		self operator--(int)
		{
			self tem(_node);
			_node = _node->_prev;
			return tem;
		}
		Ref & operator *()
		{
			return _node->_data;
		}
		Ptr& operator->()
		{
			return &operator*();
		}
		bool operator==(const self& It)const
		{
			return _node == It._node;
		}
		bool operator!=(const self& It)const
		{
			return _node != It._node;
		}

		//析构函数不用写了 list类会进行析构
	};

这里迭代器是是一个类模板,模板参数有3个,可能会有疑惑为啥是3个模板参数呢?一个模板参数应该就够用了吧,关于这点我们会一点点的分析别着急。我们先从头分析一下:首先list不能所以原生指针,我们设计list_iterator类模板来模拟原生指针。怎么模拟呢?我们可以先让llist_iterator类中有一个node类型的成员变量,我们用list_iterator中node来替代链表的头节点heda进行遍历不就好了。至于++ -- * ->这些运算符我们可以借助operator运算符重载来模拟原生指针的行为,这样不就实现了迭代器。去链表的的遍历和增删查改还是得通过头指针来挨个访问节点,迭代器的功能就是代替头节点进行遍历访问节点。我们现在想办法让迭代器称为头节点的替代品不就好了。基于此迭代器类被设计出来了。

因为我们让迭代器成为头指针的替代品,所以迭代器类就得有node*类型的成员变量。基于此我们的迭代器类只用写一个有参构造函数即可。这里为了方便书写和日后代码的更改我们将类名重命名,函数重载的返回值应该是什么呢?我们写日期类的时候运算符重载的时候返回的是日期类对象,那么迭代器类返回的就应该是迭代器类对象,所以这个类名就是我们的返回值。

关于运算符重载的细节

++就是_node=_node->next ,- -就是_node= _node->prve, * 就是 _ node->data。这些东西都是双向循环链表的特点,这里就不多说了。我们首先看到这个前置++和前置-- 因为改变的是自己本身,我们返回*this,返回值是引用。后置--和后置++返回的应该是改变前的值,这里采用是临时变量保存++或--之前的值,因为我们返回的临时变量所以采用值返回。我们解引用的时候是想拿到data数据所以这个返回应该是T。同时我们访问这个data值域的时候还可能对修改这个值域,所以返回值为引用。但是我们思考一个问题除了普通迭代器我们需要实现以外还要实现const迭代器。这个const迭代器和普通迭代器最大的区别是什么?那就是const迭代器指向的值域不能被修改。这里要注意是迭代器指向的节点中的值域不能被修改,不是迭代器不能被修改。如果迭代器不能被修改那么迭代器怎么移动遍历呢?因此在也就是在重载 * 上做文章,如果返回的是T &不就是普通迭代器,如果返回的是const T那就是const迭代器,基于此我们引入第二个模板参数Ref,这第二个模板参数是因为为了示例化出对应的cosnt迭代器和普通迭代器,这样普通迭代器和const迭代器可以公用一套代码,代码的复用性大大加强。 我们来看看下图。

在这里插入图片描述

这里我们先暂且不管第三个模板参数。我们知道迭代器是自定义类型也是一个模板类,在list模板类中 迭代器类会先实例化成list_iterator<T,T&,T * >类型或者是list_iterator<T,const T&,const * >类型,之后list再实例化的时候,对应的迭代器类型会再次实例化一次。这种就有点像模板里套个模板,这样list中的迭代器对象就会根据同一份代码实例化出两种不同的迭代器成员。这种处理方式简直是妙不可言啊,这也是库中的实现方式。

我们知道迭代器是就是模拟对应对象的指针行为进行访问或者遍历。假如实例化出list< Date>lt对象,我们知道这个Date是自定义类型,如果我们想用lt的迭代器访问lt内部的成员也就是访问值域的成员那么就得重载->。可能这样说太绕了,还是lt的例子,如果链表每个节点都是Date,那么对应的值域data就是一个Date对象,那么我们要是想访问像指针那样访问值域Date的成员就必须重载->。这也就是迭代器类第三个模板参数的由来。

在这里插入图片描述

Ptr& operator->()
		{
			return &operator*();
		}

在这里插入图片描述

这里我们看到->运算符重载的实现方式返回&operator*(),可能有点懵逼。但是我们仔细思考一下,*重载拿到的不就是节点中对应的值域,如果我们取地址那不就是节点值域对象的this。我们就可以通过->去访问自定义类型中的成员变量了。这种处理方式也十分奇妙牛逼,这也是在源码中学到的。同样的我们也知道了第三个参数的由来,这是为了支持->运算符重载。

在这里插入图片描述
在这里插入图片描述

这里迭代器我们直接是采用匿名对象的方式进行构造,_head->nex就是头节点 begin,_head就是end。这是双向循环链表特性决定的。这里typedef是先对迭代器类型重命名一次。因为库中规定了迭代器类型的命名方式。这样就轻松解决了迭代器的方式。

2.反向迭代器

我们设计好了正向迭代器,那么反向迭代器怎么设计呢?我们可能说很简单啊,我们照着正向迭代器在去写一个反向迭代器类,rbegin指向heda->prev rend指向head ,- - ++逻辑改一下 不就行了照着这个思路我们很容易写出如下思路的代码。

template<class T, class Ref, class Ptr>
struct __list_reverse_iterator
{
	typedef list_node<T> node;
	typedef __list_reverse_iterator<T, Ref, Ptr> self;
	node* _node;

	__list_reverse_iterator(node* n)
		:_node(n)
	{}

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

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

	self& operator++()
	{
		_node = _node->_prev;

		return *this;
	}

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

		return tmp;
	}

	self& operator--()
	{
		_node = _node->_next;

		return *this;
	}

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

		return tmp;
	}

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

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

在这里插入图片描述
在这里插入图片描述

这里我们将正向的迭代器++改成_node=_node->prev,- - 改成_node=_node->next就行了。但是库中并不是这样设计,库中的设计更为奇妙,库中是根据正向迭代器去适配出反向迭代器。我们看看库中的代码实现。

template<class Iterator, class Ref, class Ptr>
	struct ReverseIterator
	{
		typedef ReverseIterator<Iterator, Ref, Ptr> Self;
		Iterator _cur;

		ReverseIterator(Iterator it)
			:_cur(it)
		{}

		Ref operator*()
		{
			Iterator tmp = _cur;
			--tmp;
			return *tmp;
		}

		Self& operator++()
		{
			--_cur;
			return *this;
		}

		Self& operator--()
		{
			++_cur;
			return *this;
		}

		bool operator!=(const Self& s)
		{
			return _cur != s._cur;
		}
		bool operator==(const Self& s)
		{
			return _cur == s._cur;
		}
	};

在这里插入图片描述

有人可能会有疑问这有啥的,这不还是写了一个类吗?和上面的有区别吗?我们仔细研读一下代码,这上面这个反向迭代器完全是根据正向迭代器演化的。这里所有接口都是实际上都是调用正向迭代器接口来实现的。换句话说只要实现了正向迭代器都可以适配出正向迭代器。这样说可能还体会不到这样设计的好处,我来举个例子吧。假如我现在实现了vector的正向迭代器,现在要实现反向迭代器,如果没有采用上述方法,那么我就得设计一个反向迭代器的类在重新构思设计出一批接口。这不是很繁琐吗?如果采用库中的设计方式,不管何种容器,我只要设计出了指向迭代器,我就可以根据指向迭代器推演出反向迭代器。这就相当于复用了正向迭代器的代码,这就是妙不可言啊。

反向迭代器的++就是正向迭代器的- -,反向迭代器的- -就是正向迭代器的++,改变正向迭代器的++ - -。换句话说:我们改变正向迭代器让它原有的 - -操作变成++操作,原有的++操作变成--操作。也就是让begin end 的--变++ ++变成- -; 就是反向迭代器rbegin rend。这是上述代码核心关键之处。我们也知道begin对应rend的位置,end对应rbgein的位置。我们在list类中用begin end来构造这个rend和rbegin。

这里还有一些细节需要我们注意就是这个重载*操作符我们画图理解一下。

在这里插入图片描述

这里利用临时变量处理的很巧妙,利用临时变量先走一步解引用,这个时候迭代器迭代器是没有动的。之后迭代器再走,这样就能访问到所有节点了。这里临时变量是采用默认生成的拷贝构造,我们主要是通过节点的地址来访问节点,所以使用编译器默认生成的拷贝构造没啥问题。这里并不涉及资源的申请。这里因为使用的临时变量所以是值返回。这里虽然是传值返回在调用的正向的迭代器的*运算符重载,也就是说这个tem虽然是begin或者end的一份拷贝,但是它会根节点据地址找到对应的data值域返回值域的引用。照样可以像正向迭代器那样修改值域,因为它通过正向的迭代器适配出来的,接口都是调用的正向迭代器那一套。


3.其他接口

把最不容易实现的迭代器实现了,其余的接口都很好实现了。剩下的接口主要就是根据链表的数据结构特性来实现。

1.构造与析构

默认构造

void empty_init()
	{
		_head = new node ;
		_head->_next = _head;
		_head->_prev = _head;
	}
	//默认构造
	list()
	{
		empty_init();
	}

默认构造我们写了一个空初始化函数,这里也是仿照库中实现的。这个空初始化后续的构造也会用到。

拷贝构造

void swap(const list<T>& tem)
	{
		std::swap(_head, tem._head);
	}
	list(const list<T>& It)
	{
		empty_init();
		list<T> tem(It.begin(),It.end());
		swap(tem);
	}

拷贝构造和之前实现vector一样,先创建一个临时变量,之后通过交换临时变量的头节点即可。这里注意交换函数使用的是引用交换。也就是说_head真的被改变了。tem是通过迭代器构造的,也就是说这个tem._head真的指向一条链表。这样就实现了拷贝构造。

迭代器构造

template<class Iterator>
	list(Iterator frist, Iterator last)
	{
		empty_init();
		while (frist != last)
		{
			push_back(*frist);
			++frist;
		}
	}

我们先初始化创建一个头节点之后调用尾插接口即可,尾插的时候会创建节点,复用代码即可。

析构函数

~list()
	{
		clear();
		delete _head;
		_head = nullptr;
	}
	void clear()
	{
		iterator it = begin();
		while (it != end())
		{
			//这个it是后值++,返回是it++之后的值,但是it是没有改变的
			//不用考虑迭代器失效问题
			erase(it++);
		}
	}

这个clear函数就是把链删除至只有一个头节点为止。这里是复用的erase进行删除。这里是前置++删除,也就是返回的是++之前的值,但是it已经向前移动了。这样就不用考虑每次迭代器失效的问题了。

2.插入和删除

void insert(iterator pos,const T& val)
	{
		 node*cur = pos._node;
		 node* prev = cur->_prev;
		 node* new_node = new node(val);
		 new_node->_next = cur;
		 new_node->_prev = prev;
		 cur->_prev = new_node;
		 prev->_next = new_node;
		
	}
	iterator erase(iterator pos)
	{
		assert(pos != end());
		node* cur = pos._node;
		node* next = pos._node->_next;
		node* prev = cur->_prev;
		prev->_next = next;
		next->_prev = prev;
		delete cur;
		return iterator(next);

	}

这里插入的pos位置之前插入,删除是在指定位置删除。删除和插入逻辑这里就不做过多解释了。这里应该考虑的是迭代器失效的问题,因为插入在pos之前插入,也就说pos以前指向哪个节点现在还是指向哪个节点。唯一变化的就是在链表中的相对位置,比如pos原本指向的是链表第3个节点,插入新节点后就是链表第4个节点。这里插入不会造成迭代器失效问题。那么删除会不会造成迭代器失效问题呢?pos指向的节点都被释放了肯定会迭代器失效,所以我们设有返回值。返回的是被删除节点的next指向的节点。这里要注意一点,头节点一定不能被删除。

头尾插入删除复用

void push_back(const T& val)
	{
		insert(end(),val);
	}
	void push_front(const T& val)
	{
		insert(begin(), val);
	}
	void pop_back()
	{
		erase(--end());
	}
	void pop_front()
	{
		erase(begin());
	}

这里头尾插入删除直接复用插入删除,但是要注意位置。我们看到这个尾插,插入是在pos之前插入,end的prev指向的就是尾节点。删除是在指定位置删除,end的prev是尾,所以--end就是尾节点。

3.总结

list模拟实现重点是迭代器的设计,先实现正向迭代器,反向迭代器由正向迭代器推导出。我对const 正向迭代器和contst反向迭代器没有说太多,因为有了普通迭代器const迭代器不就很简单,要注意在list中对const迭代器类型重命名一下。这些迭代器类型都是根据迭代器类先实例化出一个类型。之后再根据list模板参数再实例化一次。以上内容如有问题,欢迎指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值