C++STL详解(五)——list类的具体实现

news2024/11/13 9:26:21

一.本次所需实现的三个类及其成员函数接口

链表首先要有结点,因此我们需要实现一个结点类。

链表要有管理结点的结构,因此我们要有list类来管理结点。

链表中还要有迭代器,而迭代器的底层其实是指针。但是我们现有的结点类无法完成迭代器的行为,因此我们还需要实现一个迭代器类。

因此,我们要实现的三个类分别是:结点类、迭代器类、链表类。

namespace trousers
{
	template<class T>
	struct _list_node
	{
		//初始化
		_list_node();
		//变量
		T data;//数值域
		_list_node<T>* prev;//前驱指针
		_list_node<T>* next;//后继指针
	};
	//迭代器    //由于list_node无法遍历和支持迭代器,因此我们需要手动实现一个迭代器版本
	           //Ref和Ptr分别代表引用和指针类型,我们可以用一个类模板重载出两个迭代器版本。(迭代器里面需要用到指针和引用),一个模板的话不够用
	template<class T, class Ref, class Ptr>
	struct _list_iterator
	{
		typedef _list_node<T> node;
		typedef _list_iterator<T,Ref,Ptr> self;
		self operator++();
		self operator++(int);
		self operator--();
		self operator--(int);
		Ref operator*();
		Ptr operator->();//_pnode->_val  operator->(*this)->_val
		self operator==(const self& t)const
		self operator!=(const self& t)const
		//变量
		node* _pnode;
	};
	//链表
	template <class T>
	class list
	{
	public:
		typedef _list_node<T> node;
		typedef _list_iterator<T,T&,T*> iterator;
		typedef _list_iterator<T, const T&, const T*> const_iterator;
		//默认成员函数
		list();
		list(const list<T>& lt);
		list<T>& operator=(const list<T>& lt);
		~list();
		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin() const;
		const_iterator end() const;
		//访问容器相关函数
		T& front();
		T& back();
		const T& front() const;
		const T& back() const;
		//插入、删除函数
		void insert(iterator pos, const T& x);
		iterator erase(iterator pos);
		void push_back(const T& x);
		void pop_back();
		void push_front(const T& x);
		void pop_front();
		//其他函数
		size_t size() const;
		void resize(size_t n, const T& val = T());
		void clear();
		bool empty() const;
		void swap(list<T>& lt);
		//私有变量
	private:
		node* _head;//指向哨兵位
	};
}

二.结点类的模拟实现

list的底层其实是一个带头双向循环链表。

因此,我们需要实现的结点类中需要的成员为:数据、前一个结点的指针、后一个结点的指针。

对于该类而言,我们不需要在类中完成任何行为,因此我们仅仅只需要实现一个构造函数即可。而该类由于都是内置成员,因此我们的析构函数可以由编译器生成。

2.1结点类的构造函数

结点类的构造函数实现起来是比较简单的,我们仅仅需要将val置想要的值,并将两个指针置空即可。

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

三.迭代器类的模拟实现

3.1迭代器的设计思路

在实现了结点类之后,我们就要开始实现迭代器类了。

由于我们无法通过只有一个参数的类模板实现const和非const的两个迭代器

因此我们这里的类模板有三个参数。

	template<class T, class Ref, class Ptr>

其中的Ref表示引用,Ptr表示解引用。 

3.2迭代器类存在的意义

在之前实现string和vector的时候,我们都不需要实现一个迭代器类,为什么实现list的时候就需要实现一个迭代器类了呢?

这是因为,string和vector对象都将数据存储在了一块连续的内存空间,我们通过指针操纵空间从而完成自增、自减、解引用等操作,然后就可以对数据进行一系列的操作,因此string和vector当中的迭代器就是原生的指针。

 但是,对于list而言,各个结点在内存中的分布并不连续,因此我们不能通过对结点的自增自减等操作来完成迭代器的行为。

而迭代存在的意义就是,让使用者不必关心底层的实现,可以用简单统一的方式对容器内的数据进行访问。

 既然list的结点指针的行为不满足迭代器的定义,那么我们就需要对结点指针进行封装,对结点指针的各个运算符进行重载,使得其支持像vector、string中的迭代器一样的操作。

举个例子,我们在用list的自增行为时,实际上是执行了p=p->next语句。

总结:list的迭代器类,实际上只是对结点的指针进行了封装,并对其各个操作符进行了重载,使得结点指针的各种行为看起来和普通指针一样。

3.3构造函数

迭代器类的构造函数实际上是对结点指针进行了封装而已,其成员变量只有一个结点的指针,因此我们的构造函数直接根据所给的结点指针构造出一个迭代器对象即可。

		_list_iterator(node* pnode)
			:_pnode(pnode)
		{}

3.4++运算符的重载

3.4.1前置++操作符

对于前置++操作符,我们的实现思路非常简单,直接将结点指针指向next,然后返回自增后的结果即可。

		self operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}
3.4.2后置++操作符

对于后置++操作符,我们采取使用一个临时变量记录该结点的方式,对结点指针完成自增,并返回临时变量即可。

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

3.5--运算符的重载

--操作符的逻辑和++操作符的逻辑基本上是一样的,只不过是将自身修改为prev而不是next。

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

3.6==运算符的重载

在使用迭代器遍历时,难免会比较两个迭代器是否相同,因此我们还需要实现==操作符。

而两个迭代器是否相同,实际上就是判断这两个迭代器是不是同一个位置上的迭代器,因此,我们只需要比较这两个迭代器的地址即可。

		bool operator==(const self& t)const
		{
			return _pnode == t._pnode;
		}

3.7!=运算符的重载

!=操作符和==操作符的作用相反,我们要判断的是这两个迭代器的地址是不是不同。

		bool operator!=(const self& t)const
		{
			return _pnode != t._pnode;
		}

3.8*运算符的重载

当我们使用*操作符时,其实就是想要得到这个地址的数据,因此我们直接返回当前指针数据所指向的数据即可,但由于我们可能会通过解引用修改数据,因此我们这里可以返回引用。

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

3.9->运算符的重载

在一些场景下,我们可能还会使用到->操作符。

譬如如下场景:

当list容器内的每个结点存储的是自定义类型时,那么当我们拿到一个位置的迭代器,我们可能还会通过->操作符来访问该类型内部的成员。

如下例:

list<vector> d;
vector<int> v1 = { 1,2,3 };
vector<int> v2 = { 4,5,6 };
vector<int> v3 (3,5);
d.push_back(v1);
d.push_back(v2);
d.push_back(v3);
list <vector>::iterator pos = it.begin();
cout << pos->size() << endl;

因此,有些情况下我们会使用到->操作符。

对于->操作符的重载,我们直接返回结点当中所存储数据的地址即可。

		Ptr operator->()//_pnode->_val  operator->(this)->_val
		{
                  //(*this)->_data;
			return &(_node->_data);
		}

说到这里,你可能会觉得有些不对,按照这种重载方式的话,我们似乎需要两个->才能调到我们想要的数据,也就是这样:

因为()内部的this是被省略的,因此我们实际上写应该是这样的

但是一个地方出现两个箭头的可读性有些过于差劲了,因此编译器在这里做了一些特殊的处理,省略了一个箭头,也就是我们所写的版本。

四.list的模拟实现

4.1默认成员函数

4.1.1构造函数

list是一个带头双向循环链表,因此在构造一个list对象时,直接申请一个头节点并让前驱指针和后继指针都指向自己即可。

		list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}
4.2.2拷贝构造函数

拷贝构造函数,我们需要先申请一个结点,然后申请一个头结点,之后我们再将原list链表一个一个通过尾插拷贝过去即可。

		list(const list<T>& lt)
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
			for (const auto& e : lt)
			{
				push_back(e);
			}
		}
4.2.3赋值运算符重载函数

赋值运算符重载和拷贝构造的实现方式是类似的,我们可以先将被赋值的链表清空然后一个一个通过尾插拷贝过去。

		list<T>& operator=(const list<T>& lt)
		{
			if (this != &lt)//防止自己给自己赋值,以避免性能浪费
			{
				clear();
				for (const auto& e : lt)
				{
					push_back(e);
				}
			}
			return *this;
		}

但是这种写法过于繁琐,我们也可以换一种思路,我们不采取引用传参,那么我们将会传入一个形参,之后我们和形参进行交换即可完成任务,而当我们的函数运行结束后还会自动销毁掉形参。

		list<T>& operator=(const list<T> lt)//编译器接受右值时自动调用其拷贝构造函数,构造出形参。
		{
			swap(lt);
			return *this;
		}
 4.2.4析构函数

对于析构函数,我们首先使用clear清理一下容器内的数据,然后将头结点释放,之后再将指针置空即可。

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

五.迭代器相关函数


begin和end

begin是返回第一个有效数据的迭代器,因此我们要返回头节点的下一个结点

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

这里需要大家注意的是,我们要返回迭代器而不是结点指针,因此我们需要用迭代器的构造函数构造出指向同一块空间的迭代器类型的匿名变量用于返回。 

end是返回最后一个有效数据的后一个数据的迭代器,在双向循环链表中要返回的是头结点

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

当然,除了这两个之外,我们还需要实现两个const版本的函数

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

		}

 六.访问容器相关函数

6.1front和back

front返回第一个有效数据,back返回最后一个有效数据,因此我们在实现front和back函数时,直接返回第一个有效数据的引用和最后一个有效数据的引用即可。

		T& front()
		{
			return *begin();
		}
		T& back()
		{
			return *(--end());
		}

除此之外,我们还需要重载一对用于const对象的front和back函数。

		const T& front() const
		{
			return *begin();//我们已经重载了解引用
		}
		const T& back() const
		{
			return *(--end());
		}

6.2插入、删除函数

6.2.1insert

对于insert函数,我们是这样写的:

  1. 首先检查一下插入位置的合法性。
  2. 然后用要插入的数据新建一个结点。
  3. 然后记录下要插入位置处结点的指针
  4. 之后建立节点之间的双向关系
		void insert(iterator pos, const T& x)
		{
			assert(pos._pnode);
			node* newnode = new node(x);
			node* cur = pos._pnode;
			node* prev = cur->_prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
			prev->_next = newnode;
		}
6.2.2erase

erase可以删除所给的迭代器位置的结点。

实现思路为:

  1. 先根据迭代器得到该位置处的结点cur
  2. 然后通过cur找到prev和next指针
  3. 之后删掉cur,并建立prev和next之间的双向关系。
  4. 返回next位置
		iterator erase(iterator pos)
		{
			assert(pos._pnode);
			assert(pos != end());
			node* cur = pos._pnode;
			node* prev = cur->_prev;
			node* next = cur->_next;
			delete cur;
			prev->_next = next;
			next->_prev = prev;
			return iterator(next);
		}
6.2.3对头尾的插入和删除函数

我们直接复用insert和erase即可。

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

七.其他函数

7.1size

对于size函数,我们可以通过迭代器的遍历获取个数。

		size_t size() const
		{
			size_t sz = 0;
			const_iterator it = begin();
			while (it != end())
			{
				it++;
				sz++;
			}
			return sz;
		}

除了这个方法外,还有一个方法可以获取到个数。

我们可以多设置一个私有成员size,在插入逻辑中,每插入一个则size+1,在删除逻辑中,每删除一个,则size-1。这样也可以获取到size 的个数。

7.2resize

resize函数规则:

  1. 若当前容器的size小于所给n,则尾插结点,直到size等于n
  2. 若当前容器的size大于所给n,则只保留前n个有效数据。

那么,如何实现resize函数呢?

  1. 首先,我们定义一个len表示链表的长度
  2. 遍历到结尾后,比较len和n的大小
  3. 若len较大,则删除掉多余的结点
  4. 若len较小,则尾插数值为x的结点。
		void resize(size_t n, const T& val = T())
		{
			iterator it = begin();
			size_t len = 0;
			while (it != end())
			{
				it++;
				len++;
			}
			while (len < n)
			{
				push_back(val);
				len++;
			}
			while (len > n)
			{
				pop_back();
				len--;
			}
		}
7.3clear

对于clear函数,我们需要做的是清空链表的有效数据。

因此我们逐个删除掉链表的结点,只保留头节点即可。

		void clear()
		{
			iterator it=begin();
			while (it != begin())
			{
				it = erase(it);//防止迭代器失效
			}
		}

7.4empty

empty函数是判空的,我们有很多种方法判断链表是否为空。

这里我们通过判断该容器的begin函数和end函数所返回的迭代器是否相同来进行判断。(如果相同,则代表只有一个头结点)

		bool empty() const
		{
			return begin() == end();
		}
7.5swap

swap函数用于交换两个容器,在list容器当中存储的实际上只有头结点的指针,因此我们只要交换一下头节点的指针即可。

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

注意点:在此处调用的swap是库中的swap,我们在swap前面加上域作用限定符,即可告诉编译器优先在全局范围内寻找swap函数。 

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

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

相关文章

在VB.net中,对数据排名次,用LINQ、SortedSet,还是用SortedList速度快

标题 在VB.net中&#xff0c;对数据排名次&#xff0c;用LINQ、SortedSet&#xff0c;还是用SortedList速度快 正文 在VB.NET中&#xff0c;选择最适合你需求的排序和索引方法时&#xff0c;需要考虑到数据的规模、是否需要频繁地更新数据结构、以及是否只需要排序结果或还需要…

【Hadoop】建立圈内组件的宏观认识

01存储02计算03调度04其他05回忆 众多组件们构建了大规模分布式计算和存储平台。本文介绍Hadoop生态圈中各个组件的主要功能和作用&#xff0c;辅助学者理解每个组件的定位和用途&#xff0c;从而建立对圈内组件的宏观认识。梳理清楚HDFS、MapReduce、YARN、Hive、HBase、Spark…

【大模型系列篇】Transformers综述--邱锡鹏

论文标题&#xff1a;A Survey of Transformers 论文作者&#xff1a;Tianyang Lin, Yuxin Wang, Xiangyang Liu, Xipeng Qiu 论文链接&#xff1a;https://arxiv.org/abs/2106.04554 Transformer 在许多人工智能领域&#xff08;如自然语言处理、计算机视觉和音频处理&#…

【区块链+金融服务】山西省信易贷平台 | FISCO BCOS应用案例

2022 年 8 月 8 日&#xff0c;山西省发展改革委按照国家的顶层设计&#xff0c;指导山西股权交易中心建设山西省信易贷平台&#xff0c;包 括三个子平台&#xff1a;一是建设集金融产品超市、融资需求精准匹配、融资监测等于一体的山西省融资综合信用服务平台&#xff1b; 二是…

微信小程序--25(WXSS模板样式了解)

一、WXSS和CSS关系 大部分特性相同 1.wxss独有 rpx尺寸单位import 样式导入 二、rpx 1.原理 rpx将所有屏幕宽度等分为750份&#xff0c;自动换成像素实现屏幕适配 2.rpx与px之间换算 约分计算不同设备比例不同 三、样式导入 1.语法 importt”相对路径“&#xff1b;…

Qt 0814作业

一、思维导图 二、登录窗口界面 自由发挥登录窗口的应用场景&#xff0c;实现一个登录窗口界面 要求&#xff1a;每行代码都有注释 【需要用到的图片或者动图&#xff0c;自己去网上找】 #include "mywidget.h"MyWidget::MyWidget(QWidget *parent): QWidget(par…

Grok-2的Beta版发布

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

高阶数据结构(Java):AVL树插入机制的探索

目录 1、概念 1.1 什么是AVL树 2.1 平衡因子 3、AVL树节点的定义 4、AVL树的插入机制 4.1 初步插入节点 4.2 更新平衡因子 4.3 提升右树高度 4.3.1 右单旋 4.3.2 左右双旋 4.4 提升左树高度 4.4.1 左单旋 4.4.2 右左双旋 5、AVL树的验证 6、AVL树的删除 1、概念 …

uni-app 使用九宫格(uni-grid)布局组件

1、运行环境 开发工具为 HBuilder X 4.23, 操作系统为 Windows 11。Vue.js 版本为 3. 2、操作步骤 首先&#xff0c;登录 HBuilder X。然后用桌面浏览器&#xff0c;访问官网组件网址。 https://ext.dcloud.net.cn/plugin?nameuni-grid 在组件网址右上角、点击“下载插…

出差学习笔记(1)汽车智能大灯一键标定功能

出差学习笔记&#xff08;1&#xff09;汽车智能大灯一键标定功能 今天看到了某公司制作的汽车智能大灯的一键标定功能&#xff0c;好奇&#xff0c;遂问之。 车前的两个大灯如何标定&#xff0c;我们可以将车辆开到一片墙前&#xff0c;将一些动态/静态图形打到墙上&#xff0…

Sublime Text常用快捷键大全

Sublime Text 是一款功能强大且广受欢迎的文本编辑器&#xff0c;其丰富的快捷键支持使得开发者能够更高效地编写和编辑代码。以下是 Sublime Text 中一些常用的快捷键&#xff0c;帮助你更加高效地使用这款工具&#xff1a; 功能分类快捷键 (Windows)快捷键 (Mac)新建文件Ctr…

Unity Shader变体优化与故障排除技巧

在 Unity 中编写着色器时&#xff0c;我们可以方便地在一个源文件中包含多个特性、通道和分支逻辑。在构建时&#xff0c;着色器源文件会被编译成着色器程序&#xff0c;这些程序包含一个或多个变体。变体是该着色器在满足一组条件后生成的版本&#xff0c;这通常会导致线性执行…

实验五之用Processing绘画

1.案例代码如下&#xff1a; import generativedesign.*; import processing.pdf.*; import java.util.Calendar; Tablet tablet; boolean recordPDF false; float x 0, y 0; float stepSize 5.0; PFont font; String letters "Sie hren nicht die folgenden Gesnge…

STM32寄存器点亮跑马灯

硬件状况 DS0灯接线方式 链路&#xff1a;3.3V --- DS0 --- LED0 --- PB5 | --- CPU 分析: PB5, 为高电平, 那么DS0灯 熄灭 PB5, 为低电平, 那么DS0灯 亮 PB5的配置 配置PB5引脚为输出模式, 输出高电平(灭), 输出低电平(亮)&#xff0c;配置为推挽输出 PB5 - GP…

前端开发攻略---图片裁剪上传的原理

目录 ​编辑 1、预览本地图片 2、图片裁剪交互 3、上传裁剪区域 1、预览本地图片 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initia…

面试必备:高频算法与面试题全面解析

干货分享&#xff0c;感谢您的阅读&#xff01; &#xff08;暂存篇---后续会删除&#xff0c;完整版和持续更新见高频面试题基本总结回顾&#xff08;含笔试高频算法整理&#xff09;&#xff09; 备注&#xff1a;引用请标注出处&#xff0c;同时存在的问题请在相关博客留言…

c++割圆法求圆周率

前言 上期的Python(加了turtle 所以带图片)割圆法点赞数量感人 但洛谷那期已经让我飞了 于是我准备掉点头发 以五升六之躯硬刚初三 这期请教了大量的高年级同学 把这个要用到九年级知识点的割圆法搞出来了 不要怕难 像我这样的xxs也能看懂 先声明 割圆法不一定要用循环 …

Open3D PCA法中特征值和特征向量中的关系及应用(原理详细版)

目录 一、概述 1.1定义 1.2特征值与特征向量的关系 1.3特征值和特征向量大小的关系 1.4在PCA法中的应用及实现步骤 二、特征值大小排序的意义 2.1排序的原因 2.2特征值大小及意义 2.3在PCA中的应用 2.4代码示例 三、特征值计算法向量 3.1评判标准 3.2原理 3…

虚拟人实时主持创意互动方案:赋能峰会论坛会议等活动科技互动感

随着增强现实、虚拟现实等技术的不断发展&#xff0c;“虚拟人实时主持”创意互动模式逐渐代替传统单一真人主持模式&#xff0c;虚拟主持人可以随时随地出现在不同活动现场&#xff0c;也可以同一时间在不同分会场中担任主持工作&#xff0c;在峰会、论坛、会议、晚会、发布会…

与LLMs进行在IDE中直接、无需提示的交互是工具构建者探索的一个有希望的未来方向

这个观点在卡内基梅隆大学与谷歌研究人员合作文章 《Using an LLM to Help With Code Understanding》 中提出。 论文地址&#xff1a;https://dl.acm.org/doi/abs/10.1145/3597503.3639187 摘要 理解代码非常具有挑战性&#xff0c;尤其是在新且复杂的开发环境中。代码注…