C++中List的实现

前言

数据结构中,我们了解到了链表,但是我们使用时需要自己去实现链表才能用,但是C++出现了list将这一切皆变为现。list可以看作是一个带头双向循环的链表结构,并且可以在任意的正确范围内进行增删查改数据的容器。list容器一样也是不支持下标访问的,主要也是物理空间并不连续。

火车侧面图 的图像结果

list的部分重要接口实现

一样的,在实现之前,我们要对list的结构有一个大概的框架

 在此基础就很容易理解list的内部是一个个节点,该节点的类型也都是自定义类型,而模版参数是节点中val的类型。代码框架如下:

namespace cr
{
	template<class T>
	struct ListNode
	{
		ListNode(const T& x = T())//T()匿名对象
			:val(x)
			,pre(nullptr)
			,next(nullptr)
		{}
		T val;
		struct ListNode<T>* pre;
		struct ListNode<T>* next;
	};

	template<class T>
	class list
	{
		typedef ListNode<T> Node;//对类进行重命名

	public:
		
	private:
		Node* _head;//自定义类型成员的指针

	};

}

1.迭代器实现

cr::list<int>::iterator it = lt.begin();
while (it != lt.end())//lt.end()临时对象
{
	cout << *it << endl;
	it++;
}

先看这段代码,该代码也就是我们想要最终实现的结果,所以我们想知道对迭代器进行哪些操作,可以先从需要实现的功能进行分析。可以初步知道迭代器“八成”是节点的指针(节点是自定义类型),所以*it和it++这两个操作铁定是需要进行运算符重载的,*it实际就是得到节点类的成员变量val的值,而it++就是通过指针的偏移,指向下一个节点。而最重要的是应该知道*it和it++操作对象是迭代器而不是list实例化出来的对象(lt),像begin和end函数面向的就是list实例化的对象,所以可以直接在list类中实现。如果*it和it++也在在list类中实现的话,默认传参传的就是list实例化对象的地址,最后默认由this指针接受。而我们需要的是对迭代器进行操作,固然需要this指向迭代器的指针。所以应该将迭代器封装起来到一个类里里更理想的状态。


所以迭代器封装成类的话,那么肯定需要的成员对象就是节点指针,而成员函数就一定有operator*和operator++。其实operator!=是最容易忽略的,仔细想想迭代器类也是一个自定义类型,这么可以直接去判断呢,所以operator!=也是要实现的,所以下边整理了需要实现的运算符重载函数以及其他的一些。

代码如下:

template<class T>
struct _List_iterator
{
	typedef ListNode<T> Node;

	_List_iterator(Node* node)
		:_pnode(node)
	{}

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

	_List_iterator<T> operator++()//前置++
	{
		_pnode = _pnode->next;
		return *this;
	}

	_List_iterator<T> operator++(int)//后置++
	{
		_List_iterator tmp = *this;
		_pnode = _pnode->next;
		return tmp;
	}

	_List_iterator<T> operator--()
	{
		_pnode = _pnode->pre;
		return *this;
	}

	_List_iterator<T> operator--(int)
	{
		_List_iterator tmp = *this;
		_pnode = _pnode->pre;
		return tmp;
	}

	bool operator!=(const _List_iterator<T>& cmpnode)const 
	{
		return _pnode != cmpnode._pnode;
	}

    T* operator->()
	{
		return &_pnode->val;//一般用于存自定义类型对象时,会隐藏一个箭头
	}

	Node* _pnode;
};


这里operator->的实现你可能会有疑问,举例下列代码:

class A
{
public:
	A(int a=0,int b=0)
		:_a(a)
		,_b(b)
	{}
	int _a;
	int _b;
};
int main()
{
	cr::list<A> a;
	a.push_back(A(1, 1));//创建匿名对象初始化
	a.push_back(A(2, 2));
	a.push_back(A(3, 3));

	return 0;
}

面对这种情况,list实例化的是一个类的对象的话,而且想要打印出该类中成员变量的话,就是下面的方法:

cr::list<A>::iterator it = a.begin();
while (it != a.end())
{
	cout << (*it)._a << (*it)._b << endl;
	it++;
}

这里的(*it)就是list中的pnode->val也就是这个A类,但是并没有重载流运算符,所以就只能去访问里面的数据(公有),所以就是通过(.)运算符,既然可以用(.)那么肯定也可以用(->)所以就重载了(->)运算符,而(->)有点不一样:

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

返回值的类型的是A*一个类型,也就是返回一个指针,所以在调用(->)运算赋不得加上两个(->)了吗,一个是调用运算符重载函数返回A*,另一个是指针访问符,所以为了增强可读性就省略一个(->)

cr::list<A>::iterator it = a.begin();
while (it != a.end())
{
	cout << it->_a << it->_b << endl;
	it++;
}

以上两种写法实现结果都是一样的:



而对于begin和end函数面向的就是list实例化的对象,那么说就是在list类中实现就行

iterator begin()
{	
	//return iterator(_head->next);//匿名对象
	return _head->next;
}
iterator end()
{
	//return iterator(_head);//匿名对象
	return _head;
}

因为迭代器封装的类是单参数,所以可以直接返回节点的指针(会自动调用迭代器的构造函数)

 2.const迭代的实现

首先我们要了解list中const迭代器与非const迭代器的区别,const迭代器中的成员变量指向的数据是一个节点的指针,并不是说const修饰了以后这个节点不能改变,实际上是节点中的val成员变量不能改变。假如说是节点不能改变的话,那么就连pre和next这两个成员变量也不可以改变,如果连这两个都不能改变的话,那么还怎么实现operator++,还怎么遍历链表中的数据呢。

所以凭这一点,const迭代器就不可能是直接在iterator前面加上const限定符修饰那么容易。

而想要val的值不能改变的话,实际上就是operator*和operator->这两个函数的返回值要加const引用来修饰


方法一:再写一个_List_const_iterator的类

对于_List_terator类而言_List_const_iterator这个类只需要改一下类型而已,像operator*和operator->的返回值加上const修饰而其他需要返回迭代器的地方改成_List_const_iterator<T>就行,最后在list类中typedef  _List_const_iterator<T>  const_iterator就完成了


方法二:通过模版实现

方法一是需要再实现一个_List_const_iterator的类,但是本质上其实这个类相较于_List_iterator没什么区别,也就是返回值的类型有区别而已,所以此时对迭代器模版的引入就在合适不过了。而此时的模版参数肯定不能只有一个参数,而应该有三个参数:

template<class T,class ref,class ptr>

模版中第二个参数的实参其实就是const T&用于operator*运算符函数的返回值,而第三个参数的实参其实就是const T* 用于operator->函数的返回值。此时你可能会问为什么是需要这两个参数呢?其实判断方法很简单,就是通过实质上的比较const迭代器和非const迭代器实现时各函数返回值有哪些发生了改变。表面上你是只写了一个迭代器的类加一个模版就实现了两个迭代器干的活,实际上在编译的时候编译器通过模版参数自动生成了两个类,在使用时会优先找最匹配的调用。

所以这里提一点:模版中类名相同模版参数不同并不代表是同一个类型,类型=类名+模版参数

迭代器实现代码

template<class T,class ref,class ptr>
	struct _List_iterator
	{
		typedef ListNode<T> Node;

		_List_iterator(Node* node)
			:_pnode(node)
		{}

		ref operator*()
		{
			return _pnode->val;
		}
		ptr operator->()
		{
			return &_pnode->val;
		}

		_List_iterator<T,ref,ptr> operator++()
		{
			_pnode = _pnode->next;
			return *this;
		}
		_List_iterator<T,ref,ptr> operator++(int)
		{
			_List_iterator tmp = *this;
			_pnode = _pnode->next;
			return tmp;
		}
		_List_iterator<T,ref,ptr> operator--()
		{
			_pnode = _pnode->pre;
			return *this;
		}
		_List_iterator<T,ref,ptr> operator--(int)
		{
			_List_iterator tmp = *this;
			_pnode = _pnode->pre;
			return tmp;
		}
		bool operator!=(const _List_iterator<T,ref,ptr>& cmpnode)const
		{
			return _pnode != cmpnode._pnode;
		}

		Node* _pnode;
	};

其实上面的代码本质上就是将operator*和operator->的返回值虚拟化,然后在调用迭代器时才进行实例化的。 

	template<class T>
	class list
	{
		typedef ListNode<T> Node;//对类进行重命名

	public:
		typedef _List_iterator<T,T&,T*> iterator;//实例化
		typedef _List_iterator<T, const T&, const T*> const_iterator;

		const_iterator begin()const
		{  
			//return iterator(_head->next);//匿名对象
			return _head->next;
		}
		const_iterator end()const
		{
			//return iterator(_head);//匿名对象
			return _head;
		}

        ......
    }

在list类中分别将两种类型迭代器重命名,所以在外面调用哪种迭代器时就会在内部模版实例化成具体的迭代器

3.list的插入与删除

        iterator insert(iterator pos,const T& x)
		{
			Node* l1 = pos._pnode->pre;
			Node* l2 = pos._pnode;
			Node* tmp = new Node(x);

			l1->next = tmp;
			tmp->pre = l1;
			tmp->next = l2;
			l2->pre = tmp;

			return tmp;
		}
		iterator erase(iterator pos)//调用之后pos节点失效
		{
			Node* l1 = pos._pnode->pre;
			Node* l2 = pos._pnode->next;

			l1->next = l2;
			l2->pre = l1;
			delete pos._pnode;

			return l2;
		}

        void push_back(const T& x)
		{
			insert(this->end(), x);
			//Node* tmp = new Node(x);
			//Node* tail = _head->pre;//找尾

			//tail->next = tmp;
			//tmp->pre = tail;
			//tmp->next = _head;
			//_head->pre = tmp;
		}

这里的插入删除同链表一样,没什么好说的,唯一要注意的就是迭代器失效的问题,list的insert不存在扩容换址,所以不会造成迭代器失效,但是list的erase就不同了,earase会释放当前的节点,所以调用之后就会失效。所以给erase一个返回值,返回需要删除的节点的下一个节点。

4.list的构造加赋值

        list()
			:_head(nullptr)
		{
			_head = new Node;//该节点也是一个类,创建时调用构造函数
			_head->next = _head;
			_head->pre = _head;
		}
		list(const list<T>& copy)
			:_head(nullptr)
		{
			_head = new Node;//该节点也是一个类,创建时调用构造函数
			_head->next = _head;
			_head->pre = _head;

			for (auto it : copy)//内部实际是用迭代器
			{
				push_back(it);
			}
		}
        list<T>& operator=(list<T> copy)//调用拷贝构造
		{
			std::swap(_head, copy._head);
			
			return *this;
		}

因为list容器类似带头双向循环链表,所以构造时要new一个带头节点,其次要注意的是new的这个节点的类型是我们自定义的类型,所以在new时就会自动调用该节点的构造函数将其空间内容初始化。赋值函数就实现的比较巧妙,通过传参调用拷贝构造函数的深拷贝,然后再换值就行。

5.空间释放析构函数

        void clear()
		{
			list::iterator del = this->begin();
			while (del != this->end())
			{
				del = erase(del);
			}
			_size = 0;
		}
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

若有不恰当的描述欢迎留言指正!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/77569.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【CSS】CSS 布局——常规流布局

<h1>基础文档流</h1><p>我是一个基本的块级元素。我的相邻块级元素在我的下方另起一行。</p><p>默认情况下&#xff0c;我们会占据父元素 100%的宽度&#xff0c;并且我们的高度与我们的子元素内容一样高。我们的总宽度和高度是我们的内容 内边距…

如何发布自己的小程序

小程序的基础内容组件 text&#xff1a; 文本支持长按选中的效果 <text selectable>151535313511</text> rich-text: 把HTML字符串渲染为对应的UI <rich-text nodes"<h1 stylecolor:red;>123</h1>"></rich-text> 小程序的…

2023牛客暑期多校训练营8-C Clamped Sequence II

2023牛客暑期多校训练营8-C Clamped Sequence II https://ac.nowcoder.com/acm/contest/57362/C 文章目录 2023牛客暑期多校训练营8-C Clamped Sequence II题意解题思路代码 题意 解题思路 先考虑不加紧密度的情况&#xff0c;要支持单点修改&#xff0c;整体查询&#xff0…

AUTOSAR NvM Block的三种类型

Native NVRAM block Native block是最基础的NvM Block&#xff0c;可以用来存储一个数据&#xff0c;可以配置长度、CRC等。 Redundant NVRAM block Redundant block就是在Native block的基础上再加一个冗余块&#xff0c;当Native block失效&#xff08;读取失败或CRC校验失…

时序预测 | MATLAB实现基于BiLSTM双向长短期记忆神经网络的时间序列预测-递归预测未来(多指标评价)

时序预测 | MATLAB实现基于BiLSTM双向长短期记忆神经网络的时间序列预测-递归预测未来(多指标评价) 目录 时序预测 | MATLAB实现基于BiLSTM双向长短期记忆神经网络的时间序列预测-递归预测未来(多指标评价)预测结果基本介绍程序设计参考资料 预测结果 基本介绍 Matlab实现BiLST…

2022年09月 C/C++(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

第1题&#xff1a;统计误差范围内的数 统计一个整数序列中与指定数字m误差范围小于等于X的数的个数。 时间限制&#xff1a;5000 内存限制&#xff1a;65536 输入 输入包含三行&#xff1a; 第一行为N&#xff0c;表示整数序列的长度(N < 100); 第二行为N个整数&#xff0c;…

把握数据要素,做数字化时代的弄潮儿

截至2022年6月&#xff0c;我国网民规模已经达到了10.51亿&#xff0c;人均上网时间达到了每周29.5个小时&#xff0c;并且这部分人群使用手机上网的比例为99.6%。如果把工作、睡眠以及其他的必要的时间算上的话&#xff0c;可以发现通过手机上网已经成为了人们日常中的一部分。…

浅谈人工智能技术与物联网结合带来的好处

物联网是指通过互联网和各种技术将设备进行连接&#xff0c;实时采集数据、交互信息的网络&#xff0c;对设备实现智能化自动化感知、识别和控制&#xff0c;给人们带来便利。 人工智能是计算机科学的一个分支&#xff0c;旨在研究和开发能够模拟人类智能的技术和方法。人工智能…

SpringBoot的配置文件以及日志设置

在使用SpringBoot开发的过程中我们通常会用到配置文件来设置配置信息 以及使用日志来进行记录我们的操作&#xff0c;方便我们对错误的定位 配置文件的作用在于&#xff1a;设置端口&#xff0c;设置数据库连接信息&#xff0c;设置日志等等 在SpringBoot中&#xff0c;配置…

【LangChain概念】了解语言链️:第2部分

一、说明 在LangChain的帮助下创建LLM应用程序可以帮助我们轻松地链接所有内容。LangChain 是一个创新的框架&#xff0c;它正在彻底改变我们开发由语言模型驱动的应用程序的方式。通过结合先进的原则&#xff0c;LangChain正在重新定义通过传统API可以实现的极限。 在上一篇博…

SpringBoot携带Jre绿色部署项目

文章目录 SpringBoot携带Jre绿色部署运行项目1. 实现步骤2. 自测项目文件目录及bat文件内容&#xff0c;截图如下&#xff1a;2-1 项目文件夹列表&#xff1a;2-2. bat内容 3. 扩展&#xff1a; 1.6-1.8版本的jdk下载 SpringBoot携带Jre绿色部署运行项目 说明&#xff1a; 实…

【Python】Web学习笔记_flask(5)——会话cookie对象

HTTP是无状态协议&#xff0c;一次请求响应结束后&#xff0c;服务器不会留下对方信息&#xff0c;对于大部分web程序来说&#xff0c;是不方便的&#xff0c;所以有了cookie技术&#xff0c;通过在请求和响应保温中添加cookie数据来保存客户端的状态。 html代码&#xff1a; …

css鼠标样式 cursor: pointer

cursor: none; cursor:not-allowed; 禁止选择 user-select: none; pointer-events:none;禁止触发事件, 该样式会阻止默认事件的发生&#xff0c;但鼠标样式会变成箭头

什么文件传输协议才能保障跨国文件传输安全又稳定

在当今的全球化时代&#xff0c;跨国文件传输是一种常见而又重要的需求&#xff0c;无论是个人还是企业&#xff0c;都需要通过网络来分享和交换各种类型和大小的文件。但是&#xff0c;跨国文件传输也面临着许多挑战和风险&#xff0c;如何选择一个合适的文件传输协议&#xf…

CUDA计算超时(TDR)和阻塞界面问题的处理参考方法

本文提供一种解决单个英伟达独立显卡(终端用户常见的情形)上计算密集导致程序崩溃和电脑界面卡死的问题参考方法,采取降低效率和花费更多时间的思路来解决崩溃和卡顿的问题,即让CPU占有率不是一直100%,也不会因为被TDR机制打断。 如上图,在GPU-Z软件中看到“GPU Load”没…

W5500-EVB-PICO 做UDP Server进行数据回环测试(七)

前言 前面我们用W5500-EVB-PICO 开发板在TCP Client和TCP Server模式下&#xff0c;分别进行数据回环测试&#xff0c;本章我们将用开发板在UDP Server模式下进行数据回环测试。 UDP是什么&#xff1f;什么是UDP Server&#xff1f;能干什么&#xff1f; UDP (User Dataqram P…

自动切换HTTP爬虫ip助力Python数据采集

在Python的爬虫世界里&#xff0c;你是否也被网站的IP封锁问题困扰过&#xff1f;别担心&#xff0c;我来教你一个终极方案&#xff0c;让你的爬虫自动切换爬虫ip&#xff0c;轻松应对各种封锁和限制&#xff01;快来跟我学&#xff0c;让你的Python爬虫如虎添翼&#xff01; 首…

解锁暑假云端生活:铁威马NAS助你打造个性化体验

暑假转眼过半&#xff0c;大家一定度过一段非常美好的时光吧。朋友圈被去各地旅游的、看各种演唱会的、各种各样的观影读后感刷屏...生活很精彩&#xff0c;但如何高效地管理、享受和分享自己的文件、照片和影音内容成为困扰我们的难题。在这方面&#xff0c;铁威马NAS成为了越…

使用python对图像加噪声

加上雨点噪声 import cv2 import numpy as npdef get_noise(img, value10):#生成噪声图像>>> 输入&#xff1a; img图像value 大小控制雨滴的多少 >>> 返回图像大小的模糊噪声图像noise np.random.uniform(0, 256, img.shape[0:2])# 控制噪声水平&#xff…

苹果支付的实现

由于app经常需要用到支付功能&#xff0c;然而ios用户&#xff0c;是无法用支付宝、微信进行支付&#xff0c;这时候只能用到苹果支付。苹果支付是苹果公司推出的一种在线支付方式&#xff0c;用户可以通过苹果支付购买应用、内购道具等等。 原理 苹果支付的实现原理是通过在…