C++——list类及其模拟实现

2024-04-08 1141阅读

前言:这篇文章我们继续进行C++容器类的分享——list,也就是数据结构中的链表,而且是带头双向循环链表。


一.基本框架

namespace Mylist
{
	template
	//定义节点
	struct ListNode
	{
		ListNode* _next;
		ListNode* _prev;
		T _data;
		ListNode(const & x = T())
			:_next(nullptr)	
			,_prev(nullptr)
			,_data(x)
		{}
	};
	template
	class list
	{
		typedef ListNode Node;
	public:
		//构造函数
		list()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		//析构函数
		~list()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
			delete _head;
			_head = nullptr;
		}
		//数据个数
		size_t size()
		{
			iterator it = begin();
			size_t Size = 0;
			while (it != end())
			{
				Size++;
				it++;
			}
			return Size;
		}
	private:
		Node* _head;
	};
}

由于要满足存储任意类型的数据,所以我们必须要使用模版来进行定义。 


迭代器

关于list类中的最难之处,就是迭代器了。

因为迭代器的原理即为指针,对于string和vector这种创建的对象的物理空间是连续的类来说,我们可以直接对迭代器进行“++”、“--”等数学运算。

而对于本质为链表的list来说,由于每个节点的物理空间都是随机创建,各个节点的地址并不连续,所以我们没法直接进行迭代器的数学运算,而需要对迭代器的各种功能进行重新定义,所以我们创建一个专门为迭代器服务的类:

	//迭代器
	template
	struct ListIterator
	{
		typedef ListNode Node;
		typedef ListIterator Self;
		ListIterator(Node* node)
			:_node(node)
		{}
		//解引用
		T& operator*()
		{
			return _node->_data;
		}
		//前置++
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		//后置++
		Self operator++(int)
		{
			Self tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		//前置--
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		//后置--
		Self operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}
		//不相等
		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
		//相等
		bool operator==(const Self& it)
		{
			return _node == it._node;
		}
		Node* _node;
	};

随后在list类中将该类名重定义为iterator,便可正常使用迭代器了:

		typedef ListIterator iterator;
		iterator begin()
		{
			return _head->_next;
		}
		iterator end()
		{
			return _head;
		}

 这里值得注意的是,因为是带头双向循环链表,所以链表的开始即哨兵位的下一个,而结尾就是哨兵位。


但是现在的迭代器是存在问题的,它并不能实现对const修饰的数据的操作,所以我们还需要一个const迭代器。因为我们的普通迭代器就是用模版来实现的,所以这里可以直接通过模版来实现const迭代器:

	//迭代器
	template
	struct ListIterator
	{
		typedef ListNode Node;
		typedef ListIterator Self;
		//构造函数
		ListIterator(Node* node)
			:_node(node)
		{}
		//解引用
		Ref operator*()
		{
			return _node->_data;
		}
		//前置++
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		//后置++
		Self operator++(int)
		{
			Self tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		//前置--
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		//后置--
		Self operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}
		//不相等
		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
		//相等
		bool operator==(const Self& it)
		{
			return _node == it._node;
		}
		Node* _node;
	};
		typedef ListIterator iterator;
		typedef ListIterator const_iterator;

这里有一个细节,因为T同时还要服务于Node类,所以不能直接对其进行修改,而是另用一个模版参数。 

 因为const对象与非const对象最大的不同之处在于对数据的访问,所以定义一个名为Ref(引用)的模版参数,来对解引用运算符重载函数进行改造。


二.常用操作

1.插入

先来看任意位置的插入,需要传入某个位置的指针pos:

		//pos前插入
		void insert(iterator pos, const T& val)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(val);
			Node* prev = cur->_prev;
			prev->_next = newnode;
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
		}

 现在对于我们来说就是非常简单,测试如下:

C++——list类及其模拟实现

这里有一个小细节,如果我们插入的位置是第一个节点之前,由于it迭代器的指向并未改变,所以如果进行遍历,他就不会遍历出我们新插入的数据,所以需要更新一下it。


有了pos位置的插入之后,就可以用它来扩展头插和尾插:

		//尾插
		void push_back(const T& x)
		{
			insert(end(), x);
		}
		//头插
		void push_front(const T& x)
		{
			insert(begin, x);
		}

2.删除

我们同样先写出pos位置的删除:

		//pos删除
		iterator erase(iterator pos)
		{
			Node* cur = pos._node;
			Node* next = cur->_next;
			Node* prev = cur->_prev;
			next->_prev = prev;
			prev->_next = next;
			delete cur;
			return iterator(next);
		}

由于删除会导致迭代器成为野指针,所以我们要对其进行更新, 测试如下:

C++——list类及其模拟实现


同样由其扩展出头删和尾删:

		//头删
		void pop_front()
		{
			erase(begin());
		}
		//尾删
		void pop_back()
		{
			erase(--end());
		}

3.拷贝

和string和vector一样,list的拷贝也需要使用深拷贝,那么它的拷贝构造函数该怎么写?

同样是要开辟新的空间,需要一个自己的头结点,随后按照被拷贝的链表的数据进行尾插即可:

		//拷贝构造
		list(const list& lt)
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			for (auto& e : lt)
			{
				push_back(e);
			}
		}

测试如下:

C++——list类及其模拟实现


此外,还有“=”运算符重载的方法:

		//交换
		void swap(list& it)
		{
			std::swap(_head, it._head);
		}
		//=运算符重载
		list& operator=(list lt)
		{
			swap(lt);
			return *this;
		}

这里仍然是巧妙的运用swap函数,因为lt是一个临时拷贝,有自己的空间和地址,所以直接让两者进行交换,lt在退出函数时即被销毁,而拷贝者则继承了它的地址空间,测试如下:

C++——list类及其模拟实现


总结

关于list类的基本知识就分享到这里啦。

因为与string和vector都存在很多相似之处,所以建议将这三者放在一起学习。

喜欢本篇文章记得一键三连,下期再见!

VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]