C++:list类(迭代器类)

news2024/12/28 21:00:52

前言

list是链表的意思

它属于链表中的带头双向循环链表

建议先掌握数据结构中的链表

C数据结构:单链表-CSDN博客

C数据结构:双向链表(带头循环)_c带头双向循环链表-CSDN博客

数据结构

首先我们需要一个链表的节点

template<class T>
struct ListNode
{
	T _data;
	ListNode* _next;
	ListNode* _prev;

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

因为这个ListNode类节点里面的内容可以是公开的,所以使用了struct默认即为public,当然也可以使用class来定义类,只需注意使用访问限定符即可

双向链表当然需要next和prev两个指针来指向后面和前面

类中还定义了一个默认构造函数用来构造节点

成员变量和成员函数放置的顺序前后都是可以的,但通常会把成员变量放到最下面

template<class T>
class list
{
    typedef ListNode<T> Node;
public:

private:
	Node* _head;
	size_t _size = 0;
};

Node*指针指向链表头节点,我们还可以定义一个size成员函数,这样可以在计算链表大小的时候减少遍历节点次数

push_back

void push_back(const T& data)
{
	Node* newnode = new Node(data);
	Node* tail = _head->prev;

	tail->_next = newnode;
	newnode->_next = _head;
	newnode->_prev = tail;
	_head->_prev = newnode;
	_size++;
}

生成新节点,将它插入到最后面即可 

push_front

void push_front(const T& data)
{
	Node* newnode = new Node;
	Node* next = _head->_next;

	_head->_next = newnode;
	newnode->_prev = _head;
	newnode->_next = next;
	next->_prev = newnode;

	insert(begin(), data);
}

生成新节点,插入到_head的下一个位置即可

默认构造函数

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

构造出我们的哨兵位头节点,并初始化next和prev指针和size大小

因为这段默认构造函数在其他的成员函数内部也可能会使用,并且默认构造函数不方便显示调用,所以我们可以把这段代码用一个函数来封装起来,方便其他成员函数使用

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

list()
{
	EmptyInit();
}

这样我们把空初始化的逻辑放到EmptyInit函数中即可

拷贝构造函数

list(const list<T>& lt)
	:_head(lt._head)
	,_size(lt._size)
{}

这是经典的错误写法! 

这里只是将lt里面的_head按字节拷贝给了this的_head(浅拷贝)

所以最终是两个_head用同一个链表,同一块空间

为了避免这个问题,我们需要重新生成一段空间来完成深拷贝

list(const list<T>& lt)
{
	EmptyInit();

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

复用EmptyInit和push_back的逻辑

因为EmptyInit会初始化出一个哨兵位头节点,我们只需要在这个头节点后面依次插入lt里面的元素即可,因为push_back也是会开新空间的

但是这里的遍历是还完不成的,因为范围for需要有begin,end,重载!=函数,这些函数又需要我们的迭代器,但是我们类中目前是还没有实现的,所以我们还需要先实现完成迭代器和这些成员函数才能完成这个拷贝构造

list迭代器

这里迭代器的实现就没有前面vector和string的那么简单了,因为它们两个的迭代器都可以通过指针的+-来遍历整个容器,但是list由于空间的不连续,所以不能简单的只是使用指针进行+-运算,而是需要我们手动完成对++,--这些运算符的重载操作

结构 

template<class T, class Ref, class Ptr>
struct list_iterator
{
	typedef ListNode<T> Node;
	typedef list_iterator<T, Ref, Ptr> Self;

	Node* _node;
};

这里需要有三个模板的支撑,前面的T代表数据类型,后面的Ref和Ptr代表的是引用和指针的意思

为什么要有这两个模板?

迭代器分为普通迭代器和const迭代器,稍后我们实现完普通迭代器后就会发现const迭代器我们需要重新拷贝一份普通迭代器的板子加上const,虽然能完成功能,但是这样代码的观赏性就不是很好,所以加上这两个模板后我们就可以在一个list_iterator类中完成iterator和const_iterator

具体讲解参考下文 

构造函数

list_iterator(Node* node)
	:_node(node)
{}

我们可以用一个node的节点来构造我们的list_iterator类

*运算符重载

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

当我们外面定义的迭代器是it = begin()时,我们*it返回的当然就需要是it里面的数据了,所以我们需要返回节点里的数据 

->运算符重载

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

->也是需要返回数据,但这里我们为什么返回的是一个数据的地址?

当我们在外面调用 it->_变量 时,编译器其实调用的是it.operator->()->_变量

这里有两个箭头,第一个箭头就是调用了我们自己写的operator函数,而第二个就是把拿到的这个数据的地址解引用拿到了里面的数据

但是为了可读性,编译器会特殊处理掉这里的一个->,所以我们只需要一个->就可以完成解引用操作

前置++运算符重载

Self& operator++()
{
	_node = _node->_next;

	return *this;
}

++后当然就是往链表里的下一个节点next走,所以我们在operator里面需要让当前iterator里面的成员变量_node指向它的next即可 

后置++运算符重载

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

	return tmp;
}

和前置++一样

但是我们需要提前用一个tmp来保存原来的位置然后返回,这也是后置++自己的特点 

前置--运算符重载

Self& operator--()
{
	_node = _node->_prev;
	return *this;
}

--当然就是往前走,也就是使用prev指针往前跳 

后置--运算符重载

Self operator--(int)
{
	Self tmp = _node;
	_node = _node->_prev;

	return tmp;
}

和后置++一样的需要tmp保存并返回tmp 

!=运算符重载

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

 判断两个迭代器不相等的逻辑当然就是比较两个节点是否是同一个

==运算符重载

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

和前面的!=运算符重载一样

这样就完成了

当我们在list类中定义迭代器iterator的时候我们是这样定义的

typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;

这样就完美的完成了iterator和const_iterator,因为iterator和const_iterator的本质就是解引用逻辑后的数据能否改变的问题,所以我们把它们定义成了两个类模板,这样就可以只用一个list_iterator类来完成iterator和const_iterator

否则我们就需要定义两个类,list_iterator和const_list_iterator来分别完成iterator和const_iterator,这样代码的可读性就降低了

list迭代器完整代码

template<class T, class Ref, class Ptr>
struct list_iterator
{
	typedef ListNode<T> Node;
	typedef list_iterator<T, Ref, Ptr> Self;
	Node* _node;

	list_iterator(Node* node)
		:_node(node)
	{}

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

	Ptr 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 = _node;
		_node = _node->_prev;

		return tmp;
	}

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

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

begin和end

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

iterator end()
{
	return _head;
}

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

const_iterator end() const
{
	return _head;
}

开始位置当然就是哨兵位头节点的下一个位置了

结束位置当然就是最后一个数据的下一个位置,也就是我们的哨兵位头节点的位置

这里明明返回值是iterator,但为什么我们可以返回一个Node*类型的指针?

C++的单参数构造函数是支持隐式类型转换的

所以我们不仅仅可以使用iterator(_head)这样的模式来构造一个iterator来返回

还可以直接返回_head来隐式类型转换成iterator类

因为这个类的构造函数就是单参数构造函数

swap

void swap(list<T> lt)
{
	std::swap(lt._head, _head);
	std::swap(lt._size, _size);
}

赋值重载函数

list<T>& operator=(list<T> lt)
{
	swap(lt);

	return *this;
}

和前面vector和string的逻辑相同,现代写法 

insert

iterator insert(iterator pos, const T& data)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(data);

	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;

	++_size;
	return newnode;
}

只需要在pos节点位置的前面插入一个节点数据为data的即可,和数据结构中的insert一致

逻辑也很简单

完成了insert之后我们是可以直接复用insert来简单的实现push_back和push_front的

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

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

erase

iterator erase(iterator pos)
{
	Node* del = pos._node;
	Node* prev = del->_prev;
	Node* next = del->_next;

	prev->_next = next;
	next->_prev = prev;
	
	--_size;
	delete del;
	return next;
}

erase的逻辑也是和数据结构中的erase一致,思路比较简单

要注意的就是注意释放掉删除的那块空间,防止内存泄漏

所以完成了erase,那么pop_back和pop_front就很容易了

迭代器失效问题

list的insert和erase也是存在迭代器失效的问题的,具体细节可以参考vector中的迭代器失效问题

C++:vector类(default关键字,迭代器失效)-CSDN博客

pop_back

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

pop_front

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

clear

void clear()
{
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
		// it++ 错误写法:erase后迭代器失效
	}
}

循环遍历整个链表,将除了哨兵位头节点的其他节点全部删除,删除哨兵位头节点的任务当然是交给析构函数了

只需要复用erase删除即可

析构函数

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

 首先复用clear将除了哨兵位头节点的全部节点删除

然后手动删除哨兵位头节点即可

size

size_t size() const
{
	return _size;
}

empty

bool empty() const
{
	return _size == 0;
}

完整代码

#pragma once

#include<iostream>
using namespace std;

namespace lyw
{
	template<class T>
	struct ListNode
	{
		T _data;
		ListNode* _next;
		ListNode* _prev;

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

	template<class T, class Ref, class Ptr>
	struct list_iterator
	{
		typedef ListNode<T> Node;
		typedef list_iterator<T, Ref, Ptr> Self;
		Node* _node;

		list_iterator(Node* node)
			:_node(node)
		{}

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

		Ptr 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 = _node;
			_node = _node->_prev;

			return tmp;
		}

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

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

	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;

		iterator begin()
		{
			//return iterator(_head->_next);
			return _head->_next; // 单参数构造函数支持隐式类型转换
		}

		iterator end()
		{
			return _head;
		}

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

		const_iterator end() const
		{
			return _head;
		}

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

		list()
		{
			EmptyInit();
		}

		list(initializer_list<T> il)
		{
			EmptyInit();
			for (auto& x : il)
			{
				push_back(x);
			}
		}

		// 浅拷贝
		//list(const list<T>& lt)
		//	:_head(lt._head)
		//	,_size(lt._size)
		//{}
		
		list(const list<T>& lt)
		{
			EmptyInit();

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

		list<T>& operator=(list<T> lt)
		{
			swap(lt);

			return *this;
		}

		void swap(list<T> lt)
		{
			std::swap(lt._head, _head);
			std::swap(lt._size, _size);
		}

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

		void clear()
		{
			auto it = begin();
			while (it != end())
			{
				it = erase(it);
				// it++ 错误写法:erase后迭代器失效
			}
		}

		iterator insert(iterator pos, const T& data)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(data);

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			++_size;
			return newnode;
		}

		iterator erase(iterator pos)
		{
			Node* del = pos._node;
			Node* prev = del->_prev;
			Node* next = del->_next;

			prev->_next = next;
			next->_prev = prev;
			
			--_size;
			delete del;
			return next;
		}

		void push_back(const T& data)
		{
			//Node* newnode = new Node(data);
			//Node* tail = _head->prev;

			//tail->_next = newnode;
			//newnode->_next = _head;
			//newnode->_prev = tail;
			//_head->_prev = newnode;
			//_size++;

			insert(end(), data);
		}

		void push_front(const T& data)
		{
			//Node* newnode = new Node;
			//Node* next = _head->_next;

			//_head->_next = newnode;
			//newnode->_prev = _head;
			//newnode->_next = next;
			//next->_prev = newnode;

			insert(begin(), data);
		}

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

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

		size_t size() const
		{
			return _size;
		}

		bool empty() const
		{
			return _size == 0;
		}
	private:
		Node* _head;
		size_t _size = 0;
	};
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ThinkPHP5漏洞分析之代码执行

漏洞概要 本次漏洞存在于 ThinkPHP 的缓存类中。该类会将缓存数据通过序列化的方式&#xff0c;直接存储在 .php 文件中&#xff0c;攻击者通过精心构造的 payload &#xff0c;即可将 webshell 写入缓存文件。缓存文件的名字和目录均可预测出来&#xff0c;一旦缓存目录可访问…

【张】#12 enum 枚举

enum 枚举定义格式&#xff1a; enum <类型名> {<枚举常量表> }; 枚举其实就是一个整数 enum example {Aa,Bb10,Cc //给Bb赋值为10后&#xff0c;Cc的值会变成11 }; 枚举变量只能使用枚举值&#xff0c;枚举可以赋值给整型&#xff0c;整型不能赋值给枚举 #inc…

掌握Jenkins自动化部署:从代码提交到自动上线的全流程揭秘

Jenkins自动化部署是现代软件开发中不可或缺的一部分&#xff0c;它不仅简化了代码的发布过程&#xff0c;还为整个团队带来了无与伦比的效率和协作力。想象一下&#xff0c;开发者们可以专注于编写高质量的代码&#xff0c;而不是为繁琐的手动部署所烦恼&#xff1b;测试人员能…

力扣高频SQL 50题(基础版)第四十四题之626. 换座位

文章目录 力扣高频SQL 50题&#xff08;基础版&#xff09;第四十四题之626. 换座位626. 换座位题目说明思路分析实现过程准备数据实现方式结果截图 力扣高频SQL 50题&#xff08;基础版&#xff09;第四十四题之626. 换座位 626. 换座位 题目说明 表: Seat --------------…

<数据集>街头摊贩识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;966张 标注数量(xml文件个数)&#xff1a;966 标注数量(txt文件个数)&#xff1a;966 标注类别数&#xff1a;1 标注类别名称&#xff1a;[street-vendor] 序号类别名称图片数框数1street-vendor9662016 使用标注…

Java流程控制02:if选择结构

本节内容教学视频链接&#xff1a;Java流程控制04&#xff1a;if选择结构_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV12J41137hu?p36&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5 在Java中&#xff0c;if 选择结构用于根据特定条件执行不同的代码块。 if语句有四…

CRC校验算法详解、C语言实现

一、前言 1.1 CRC算法介绍 CRC&#xff08;Cyclic Redundancy Check&#xff09;校验算法是一种广泛应用于数据通信和存储系统中的错误检测方法&#xff0c;主要用于检测数据在传输过程中是否发生了改变。CRC算法通过计算一个固定长度的校验码&#xff0c;将该校验码附加到原…

Zookeeper使用快速入门:基础命令,wacth监控,权限控制

目录 前置知识 1. 基础命令 未知指令&#xff1a; ls&#xff1a; create&#xff1a; zookeeper中节点有四种类型&#xff0c;分别是&#xff1a; 1. 持久节点&#xff08;Persistent Node&#xff09; 2. 临时节点&#xff08;Ephemeral Node&#xff09; 3. 持久顺序…

进程间通信 ---共享内存

序言 在前一篇文章中&#xff0c;我们介绍了名为 &#x1f449;管道 的进程间通信的方式&#xff0c;该种方式又可分为 匿名管带&#xff0c;命名管道。前者最大的特点就是 仅支持包含血缘关系两进程之间的通信&#xff0c;而后者 支持任意进程间的通信。  在本篇文章中&…

python3.9+wxPython设计的一个简单的计算器

运行环境&#xff1a;python3.9wxPython4.2.1 运行效果&#xff1a; 按下等于号&#xff0c;输出&#xff1a; 按下R键&#xff0c;保留两位小数 键盘布局与逻辑分离&#xff0c;添加删除功能一般功能或修改键盘布局只需要更改词典的顺序即可。添加特殊功能时则需要将队对应的…

【kubernetes】k8s配置资源管理

一、ConfigMap资源配置 ConfigMap保存的是不需要加密配置的信息 ConfigMap 功能在 Kubernetes1.2 版本中引入&#xff0c;许多应用程序会从配置文件、命令行参数或环境变量中读取配置信息。ConfigMap API 给我们提供了向容器中注入配置信息的机制&#xff0c;ConfigMap 可以被…

基于vue框架的CKD电子病历系统nfa2e(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;患者,医生,药品信息,电子病历,临时医嘱,长期医嘱,健康科普 开题报告内容 基于Vue框架的CKD电子病历系统 开题报告 一、选题背景 随着信息技术的飞速发展和医疗信息化的深入推进&#xff0c;电子病历系统&#xff08;Electronic Medic…

SpringBoot事务-调度-缓存

一.Spring Boot中的事务管理 设置事务 Transactional(isolation Isolation.DEFAULT) Transactional(propagation Propagation.REQUIRED) 开启事务 EnableTransactionManagement ​​​​​​​ 1. 开启事务管理 要开启 Spring 的事务管理&#xff0c;你需要在你的 Spring B…

Docker 日志管理

一、ELK -Filebeat Elasticsearch 数据的存储和检索 常用端口&#xff1a; 9100&#xff1a;elasticsearch-head提供web访问 9200&#xff1a;elasticsearch与其他程序连接或发送消息 9300&#xff1a;elasticsearch集群状态 Logstash 有三个组件构成input&#xff0c;fi…

网安行业薪资:「3人拿4干5」

吉祥知识星球http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247485367&idx1&sn837891059c360ad60db7e9ac980a3321&chksmc0e47eebf793f7fdb8fcd7eed8ce29160cf79ba303b59858ba3a6660c6dac536774afb2a6330#rd 《网安面试指南》http://mp.weixin.qq.com/s…

计算机毕业设计 农家乐管理平台 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

Linux | Linux开发工具链全攻略:yum、vim、gcc/g++、GDB、Makefile与git版本控制

目录 Linux开发环境全解析&#xff1a;工具、编程与版本控制 1、软件包管理器YUM 查看可用的软件包 安装软件包 更新软件包 卸载软件包 查找软件包信息 清理缓存 检查可更新的软件包 显示软件包的依赖关系 2、Vim编辑器 vim的三种模式&#xff1a;命令模式、插入模…

计算机基础知识复习8.13

cookie和session区别 cookie:是服务器发送到浏览器&#xff0c;并保存在浏览器端的一小块数据 浏览器下次访问服务时&#xff0c;会自动携带该块数据&#xff0c;将其发送给服务器 session:是javaEE标准&#xff0c;用于在服务端记录客户端信息 数据存放在服务端更加安全&a…

Leetcode JAVA刷刷站(14)最长公共前缀

一、题目概述 二、思路方向 在Java中&#xff0c;要编写一个函数来查找字符串数组中的最长公共前缀&#xff0c;我们可以遵循以下步骤&#xff1a; 处理边界条件&#xff1a;如果数组为空或长度为0&#xff0c;直接返回空字符串。初始化最长公共前缀&#xff1a;将数组的第一个…

[0CTF 2016]piapiapia1

打开题目 看到登录口 字符串绕过长度限制strlen($_POST[nickname]) > 10