【C++STL详解(六)】--------list的模拟实现

news2024/12/24 3:10:05

目录

前言

一、接口总览

一、节点类的模拟实现

二、迭代器类的模拟实现

迭代器的目的

list迭代器为何要写成类?

迭代器类模板参数说明

模拟实现

1.构造函数

2.*运算符重载

3.->运算符重载

4.前置++

5.后置++

6.前置--

7.后置--

8.!=

9.==

三、list类的模拟实现

Ⅰ、默认成员函数

​编辑

1.构造函数

2.拷贝构造

3.赋值重载

4.析构函数

二、迭代器

begin+end

三、访问数据

front和back

四、增删查改

1.insert()

2.erase()

3.push_back

4.pop_back()

5push_front

6.pop_front

7.clear()

8.swap()

五、容量

size

empty()


前言

对于list的模拟实现重点是其迭代器的实现,同时这又是对C++类与对象的一次更深刻的理解与体会!

一、接口总览

namespace li
{
	//节点结构
	template<class T>
	struct ListNode
	{
        //成员变量
		ListNode<T>* _prev;
		ListNode<T>* _next;
		T _date;

		ListNode(const T& value = T());//构造函数
	};


    //迭代器类
    template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;
		Node* _node;

		ListIterator(Node* node);
		Ref operator*();

		Ptr operator->();

		//++it
		Self& operator++();

		//it++
		Self operator++(int);

		//--it
		Self& operator--();

		//it--
		Self operator--(int);

		//it1!=it2
		bool operator!=(const Self& it);
		//it1==it2
		bool operator==(const Self& it);
	};

	//list结构
	template<class T>
	class list
	{
	public:
		typedef ListNode<T>  Node;
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;

		void empty_init()
		// //iterator///
		iterator begin();
		iterator end();
		const_iterator begin()const;
		const_iterator end()const;

		// /

		// ///构造//
		list();
		list(int n, const T& value = T());

		//迭代器区间初始化
		template <class Iterator>
		list(Iterator first, Iterator last);

		//拷贝构造
		//it1(it2
		list(const list<T>& l);

		//赋值构造
		//it1=it2
		list<T>& operator=(list<T> l);
		//析构函数
		~list();
		

		//modify
		iterator insert(iterator pos, const T& x);
		iterator erase(iterator pos);
		void push_back(const T& x);
		void pop_back();
		void push_front(const T& val);
		void pop_front();
		void clear();

		void swap(list<T>& l);

		///
		// List Capacity
		size_t size()const;
		bool empty()const;

		
		// List Access
		T& front();
		const T& front()const;
		T& back();
		const T& back()const;


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

同样要再自己的命名空间域里面模拟实现!!!

一、节点类的模拟实现

前面我们也有说到list实际上底层就是一个带头双向循环链表,链表的结构就是由一个又一个的结点组成,但是呢,list每次实例化出来的对象数据类型可能不一样,因此我们首先需要实现一个结点类!每一个结点所包含的信息有:数据、前驱指针、后继指针!同时类里面只需要实现一个构造函数。因为该结点类只需根据数据类型去构造去一个结点即可!

//结点类
template<class T>
struct ListNode
{
	ListNode<T>* _prev;//前驱指针
	ListNode<T>* _next;//后继指针
	T _date;//数据

	ListNode(const T& value = T())//全缺省
		:_prev(nullptr)
		, _next(nullptr)
		, _date(value)
		{}
};

对于构造函数里面的参数在vector的模拟实现有提及,如果有传值,那就用对应类型的值即可,如果没有,那就使用对应类型的默认构造函数所构造出来的值作为数据!

注意:这个用关键字struct的类,默认的成员变量和成员函数是公开的,类内类外都可以访问!

二、迭代器类的模拟实现

迭代器的目的

在解释原因之前,我们需要了解到一点就是迭代器的意义或者说目的到底是什么?

实际上,迭代器的目的就是:不关注底层的实现细节,能够采用一种类似于指针的方式去访问容器中的内容和数据!说白了就是想要去模拟指针的行为!(++,--,*等操作)

list迭代器为何要写成类?

在前面的string和vector的模拟实现中我们都没有单独的去实现这样一个迭代器类,为啥这里需要呢?实际上就是因为底层空间结构,string和vector是一段连续的空间,他们底层的迭代器就是原生的指针

而list底层的结构是不连续的,随机的,如果采用直接采用原生的指针结点去作为迭代器,那么对于++这类的操作符,一个结点能进行吗?很显然是不能的,因为不是连续的空间!

所以,对于list的迭代器,虽说本质还是原生的结点指针,但是不能直接采用,因为原生的结点指针不能够满足我们所需要的迭代器的行为,也就是它不能像vector和string的迭代器那样直接进行++,--,*等操作!

所以的所以,内置类型不能满足我们所需要的行为时,我们可以将其封装成自定义类型,也就是将原生的结点指针封装成一个类,这样就变成了自定义类型,对于一个类,我们就可以进行运算符的重载!比如表面上是对迭代器进行++操作,其底层实际上是node=node->next!这不是就符合迭代器存在的目的吗?

总结:list迭代器,由于原生结点不能满足我们所需的行为,因此要将其封装成一个类,也就是变成自定义类型,就能控制它的行为!

迭代器类模板参数说明

template<class T, class Ref, class Ptr>

这个参数的存在就是就是因为迭代器实际有两种,一个是非const,一个为const对象提供的。而这两种迭代器的底层实现起来其实大差不差,为了让代码不冗余,所以去定义了这个重定义类型。可以看list类中的这几个重定义类型

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

可以看到Ref对应的是T引用,Ptr对应的就是T指针,他们会根据传进来的类型自动匹配!如果不设计就很难区分。

为啥要一个引用,一个指针呢?实际和运算符重载有关,接着向下看!

模拟实现

1.构造函数

本质还是结点指针,所以迭代器需要传进来一个结点指针完成构造!

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

2.*运算符重载


//*it
Ref operator*()
{
    return _node->_date;
}

这个操作实际上相当于指针的解引用,*it,访问数据,对于解引用操作,我们不仅可以对当前是数据进行读操作,还能重新赋值,也就是可读可写,所以采用引用返回!所以原本是T&,但由于要区分const T&,就用了Ref这个模板参数,这就是它的由来

3.->运算符重载

这个的存在,在某些场景下面用迭代器是可能会用到->操作!

如下场景:

如我们的容器里面的数据类型不是内置类型,而是自定义类型时,可能会使用到这个运算符

比如上面这段代码,在*it时会发生错误,因为解引用只是访问到A而已,没有访问到里面的成员变量

改法1:

改法2:

这里就需要使用到运算符->重载,实际上这里完整的写法是:it.operator->()->_a,缩写就是两个->->,第一个->实际上获取的是A*,第二个是对A*指针的解引用!编译器为了代码可读性,省略了一个!

有上面的知识可以得出重载的底层实现了,实际上就是返回当前数据的地址

//为了像指针那样去操作:it->;
Ptr operator->()
{
	return &_node->_date;
}

这里原本的返回类型是T*的,但是由于要区分const T*, 所以设计一个模板参数叫做Ptr,它会根据传进来的数据类型进行实例化,这也是它由来的原因!

注意:-->操作要求成员变量是公有的才可以访问!

4.前置++

前置++要求是当前对象先自增完才返回自增后的数据,自增的操作实际上就是让指针向后移动

//++it
Self& operator++()
{
	_node = _node->_next;
	return *this;
}

注意:Self为当前迭代器的类型

typedef ListIterator<T, Ref, Ptr> Self;

5.后置++

后置++,要先返回加之前的结果,然后再++,所以可以拷贝构造出一个临时变量,然后再自增,最后再返回这个临时变量即可!注意了,这里不能用引用返回,而是使用传值返回,因为临时变量具有常性的原因,可读不可写

//it++
Self operator++(int)
{
	Self tmp(*this);//虽然这个类,没有拷贝构造,但是默认生成的拷贝构造就已经够用了,因为迭代器只需要浅拷贝就好了
	_node = _node->_next;
	return tmp;
}

需要注意后置的写法!!!这个再类和对象二有提及

6.前置--

这个和前置++一样的思路,让指针前移即可!

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

7.后置--

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

8.!=

注意这里是比较两个结点的指针,不是里面的数据,如果比较数据,那就扯淡了,因为可能数据会相同!

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

9.==

也是比较指针!

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

其实可以看出上一章list的介绍与使用中,为什么,list的迭代器只能++,而不能是begin()+5等等之类的了,因为底层压根没有重载,同时它本身的结构也不支持!

容器之间的迭代器底层实现是不一样的,这取决于他们的底层结构,但是表面上使用起来类似,都是去模仿指针的行为!

三、list类的模拟实现

Ⅰ、默认成员函数

成员变量无需多言,就是一个结点指针,外加一个size!以及额外的三个重命名类型!

public:
typedef ListNode<T>  Node;
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;

//成员变量
private:
Node* _head;
size_t _size;

除此之外,我们新设置了个成员函数初始化空链表的,因为每次构造都需要先构造一个空的链表,然后再进行后续操作!

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

1.构造函数

  • 无参数构造
list()
{
	empty_init();
}
  • 特定值初始化
list(int n, const T& value = T())
{
	empty_init();
	for (int i = 0; i < n; i++)
	{
		push_back(value);//尾插数据即可
	}
}
  • 迭代器区间初始化
template <class Iterator>
list(Iterator first, Iterator last)
{
	empty_init();
	while (first != last)
	{
		push_back(*first);
		++first;
	}

}

这里和vector的类似!

2.拷贝构造

先初始化一个头,再尾插即可!

//拷贝构造
//it1(it2)
list(const list<T>& l)
{
	empty_init();
	for (auto& e : l)
	{
		push_back(e);
	}
}

3.赋值重载

这里我们采用现代写法!使用复用拷贝构造+swap交换

//赋值构造
//it1=it2
list<T>& operator=(list<T> l)//引用返回支持连续赋值
{
	swap(l);
	return *this;
}

这里也可以采用类似 string类中传统写法,先清理容器,再一个个尾插!

//赋值构造
//it1=it2
list<T>& operator=(const list<T>& l)
{
	if (this != &l)//避免自己给自己赋值
	{
		clear();//清理容器
		for (auro& e : l)
		{
			push_back(e);//尾插到it1
		}
	}
	return *this;

}

4.析构函数

先清理内容,再释放头结点!

//析构函数
~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

二、迭代器

begin+end

需知:begin():返回的是第一个有效数据的迭代器

end():返回的是最后一个有效数据的下一个位置的迭代器

iterator begin()
{
	return iterator(_head->_next);//会去调用迭代器类构造一个迭代器,再返回
    //return _head->_next;也可以这样去写,因为单参数的构造函数支持隐式类型的转化
    //而iterator的构造函数实际上就是单参数的构造
}

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


//const迭代器
const_iterator begin()const
{
	return iterator(_head->_next);
}

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

对于const迭代器,不能写成const iterator,这样性质就不同了,因为这个表示的是迭代器本身不能修改,而我们期望的是指向的内容不能修改,所以const iterator不是我们需要的迭代器!

一定要注意区分这里的iterator和vector中的iterator,这里的iterator已经被重命名了,它实际上是一个类!

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

三、访问数据

front和back

front:返回list的第一个结点中值的引用,就是取头数据

back:返回list的最后一个结点中值的引用,就是取尾数据

T& front()
{
	return _head->_next->_date;
}
const T& front()const
{
	return _head->_next->_date;
}
T& back()
{
	return _head->_prev->_date;
}
const T& back()const
{
	return _head->_prev->_date;
}

四、增删查改

1.insert()

在pos位置前插入操作,并返回pos位置的迭代器!这个pos实际上也是个迭代器!具体可以看上节的原函数list的介绍与使用!这个只模拟实现一个就是插入值的操作!

这里的插入,实际上就是new一个结点出来,在进行插入操作即可!

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

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

	++_size;

	return pos;
}

2.erase()

删除pos位置的值,并返回pos位置的迭代器。所以这里会存在迭代器失效的问题,注意只是当前迭代器失效,之后的其他迭代器并不会受到任何影响!因为每一个迭代器都是由一个个结点构造出来的!

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

	prev->_next = next;
	next->_prev = prev;
	delete cur;
	--_size;

	return next;
}

需要注意删除操作,我们需要先保存当前结点的前一个结点和后一个结点,这样才能将其前后的两个结点连接起来!

3.push_back

尾插操作,实际上就是在end()位置插入,就是头结点的前面插入!直接复用insert

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

4.pop_back()

尾删操作,直接复用erase即可!

void pop_back()
{
	erase(--end());//这里只能--end,不能end-1因为end是传值返回的
}

5push_front

头插操作,直接复用!

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

6.pop_front

头删操作,直接复用!

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

7.clear()

清理工作,保留头节点!

void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it=erase(it);//注意这里一定要更新迭代器,因为会失效
	}
}

8.swap()

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

直接复用全局的swap

五、容量

size

这里我们在插入和删除操作,都加上了++size或者--size了。所以直接调用即可!

size_t size()const
{
	return _size;
}

empty()

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

好了今天的分享就到这里,我认为这是我学C++的第二个难点,重点就是迭代器类的模拟实现,它再一次体现了类和对象的特点以及封装的特性。值得反复去体会和反思!!!

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

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

相关文章

【知识加油站】——机电产品数字孪生机理模型构建

明确一种多领域、多层次、参数化、一致性的机电一体化装备数字孪生机理模型构建准则&#xff01; 关键词英文简称&#xff1a; 数字孪生&#xff1a;DT物联网&#xff1a;IoT网络物理系统&#xff1a;CPS高级架构&#xff1a;HLA统一建模语言&#xff1a;UML数控机床&#xf…

2-qt之信号与槽-简单实例讲解

前言、因实践课程讲解需求&#xff0c;简单介绍下qt的信号与槽。 一、了解信号与槽 怎样使用信号与槽&#xff1f; 概览 还记得 X-Window 上老旧的回调函数系统吗&#xff1f;通常它不是类型安全的并且很复杂。&#xff08;使用&#xff09;它&#xff08;会&#xff09;有很多…

精析React与Vue架构异同及React核心技术——涵盖JSX、组件、Props、State、生命周期与16.8版后Hooks深化解析

React&#xff0c;Facebook开源的JavaScript库&#xff0c;用于构建高性能用户界面。通过组件化开发&#xff0c;它使UI的构建、维护变得简单高效。利用虚拟DOM实现快速渲染更新&#xff0c;适用于单页应用、移动应用&#xff08;React Native&#xff09;。React极大推动了现代…

【链表】:链表的带环问题

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;数据结构 &#x1f337;追光的人&#xff0c;终会万丈光芒 前言&#xff1a; 链表的带环问题在链表中是一类比较难的问题&#xff0c;它对我们的思维有一个比较高的要求&#xff0c;但是这一类…

51单片机入门:DS1302时钟

51单片机内部含有晶振&#xff0c;可以实现定时/计数功能。但是其缺点有&#xff1a;精度往往不高、不能掉电使用等。 我们可以通过DS1302时钟芯片来解决以上的缺点。 DS1302时钟芯片 功能&#xff1a;DS1302是一种低功耗实时时钟芯片&#xff0c;内部有自动的计时功能&#x…

Spring Boot:国际化

Spring Boot 前言国际化 前言 在 Spring MVC&#xff1a;视图与视图解析器 的文章中&#xff0c;介绍过使用 Jstl 的 fmt 标签实现国际化&#xff0c;Spring MVC 会把视图由 InternalResourceViewResolver 转换为 JstlView&#xff08;InternalResourceView 的子类&#xff09…

【DPU系列之】如何通过带外口登录到DPU上的ARM服务器?(Bluefield2举例)

文章目录 1. 背景说明2. 详细操作步骤2.1 目标拓扑结构2.2 连接DPU带外口网线&#xff0c;并获取IP地址2.3 ssh登录到DPU 3. 进一步看看系统的一些信息3.1 CPU信息&#xff1a;8核A723.2 内存信息 16GB3.3 查看ibdev设备 3.4 使用小工具pcie2netdev查看信息3.5 查看PCIe设备信息…

【JavaEE 初阶(一)】初识线程

❣博主主页: 33的博客❣ ▶️文章专栏分类:JavaEE◀️ &#x1f69a;我的代码仓库: 33的代码仓库&#x1f69a; &#x1faf5;&#x1faf5;&#x1faf5;关注我带你了解更多线程知识 目录 1.前言2.进程3.线程4.线程和进程的区别5.Thread创建线程5.1继承Thread创建线程5.2实现R…

【深度优先搜索 图论 树】2872. 可以被 K 整除连通块的最大数目

本文涉及知识点 深度优先搜索 图论 树 图论知识汇总 LeetCode 2872. 可以被 K 整除连通块的最大数目 给你一棵 n 个节点的无向树&#xff0c;节点编号为 0 到 n - 1 。给你整数 n 和一个长度为 n - 1 的二维整数数组 edges &#xff0c;其中 edges[i] [ai, bi] 表示树中节点…

课题学习(二十三)---三轴MEMS加速度计芯片ADXL372

声明&#xff1a;本人水平有限&#xff0c;博客可能存在部分错误的地方&#xff0c;请广大读者谅解并向本人反馈错误。 一、基础配置 测量范围-200g-200g&#xff0c;分辨率为12位&#xff0c; V s 、 V D D I / O V_s、V_{DDI/O} Vs​、VDDI/O​范围为1.6V-3.5V 1.1 引脚配…

Apache和Nginx的区别以及如何选择

近来遇到一些客户需要lnmp环境的虚拟主机&#xff0c;但是Hostease这边的虚拟主机都是基于Apache的&#xff0c;尽管二者是不同的服务器软件&#xff0c;但是大多数情况下&#xff0c;通过适当的配置和调整两者程序也是可以兼容的。 目前市面上有许多Web服务器软件&#xff0c;…

Microsoft Remote Desktop Beta for Mac:远程办公桌面连接工具

Microsoft Remote Desktop Beta for Mac不仅是一款远程桌面连接工具&#xff0c;更是开启远程办公新篇章的利器。 它让Mac用户能够轻松访问和操作远程Windows计算机&#xff0c;实现跨平台办公的无缝衔接。无论是在家中、咖啡店还是旅途中&#xff0c;只要有网络连接&#xff0…

Windows平台通过MobaXterm远程登录安装在VMware上的Linux系统(CentOS)

MobaXterm是一个功能强大的远程计算工具&#xff0c;它提供了一个综合的远程终端和图形化的X11服务器。MobaXterm旨在简化远程计算任务&#xff0c;提供了许多有用的功能&#xff0c;使远程访问和管理远程服务器变得更加方便&#xff0c;它提供了一个强大的终端模拟器&#xff…

【人工智能基础】RNN实验

一、RNN特性 权重共享 wordi weight bais 持久记忆单元 wordi weightword baisword hi weighth baish 二、公式化表达 ht</sub f(ht - 1, xt) ht tanh(Whhht - 1 Wxhxt) yt Whyht 三、RNN网络正弦波波形预测 环境准备 import numpy as np import torch …

如何快速找出文件夹里的全部带有中文纯中文的文件

首先&#xff0c;需要用到的这个工具YTool&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 步骤 1、打开工具&#xff0c;切换到批量复制文件 2、鼠标移到右侧&#xff0c;点击搜索添加 3、设定查找范围、指定为文件、勾选 包含全部子文件夹&…

macOS DOSBox 汇编环境搭建

正文 一、安装DOSBox 首先前往DOSBox的官网下载并安装最新版本的DOSBox。 二、下载必备的工具包 在用户目录下新建一个文件夹&#xff0c;比如 dosbox: mkdir dosbox然后下载一些常用的工具。下载好了后&#xff0c;将这些工具解压&#xff0c;重新放在 dosbox 这个文件夹…

微服务---feign调用服务

目录 Feign简介 Feign的作用 Feign的使用步骤 引入依赖 具体业务逻辑 配置日志 在其它服务中使用接口 接着上一篇博客&#xff0c;我们讲过了nacos的基础使用&#xff0c;知道它是注册服务用的&#xff0c;接下来我们我们思考如果一个服务需要调用另一个服务的接口信息&…

ICode国际青少年编程竞赛- Python-1级训练场-识别循环规律1

ICode国际青少年编程竞赛- Python-1级训练场-识别循环规律1 1、 for i in range(4):Dev.step(6)Dev.turnLeft()2、 for i in range(3):Dev.turnLeft()Dev.step(2)Dev.turnRight()Dev.step(2)3、 for i in range(3):Spaceship.step(5)Spaceship.turnLeft()Spaceship.step(…

MySQL: Buffer Pool概念整理

一. 简介 MySQL中的Buffer Pool是InnoDB存储引擎用来缓存表数据和索引的内存区域。这是InnoDB性能优化中最关键的部分之一。通过在内存中缓存这些数据&#xff0c;InnoDB可以极大减少对磁盘I/O的需求&#xff0c;因为从内存中读取数据远比从磁盘读取要快得多。因此&#xff0c…

如何修复连接失败出现的错误651?这里提供修复方法

错误651消息在Windows 7到Windows 11上很常见&#xff0c;通常会出现在一个小的弹出窗口中。实际文本略有不同&#xff0c;具体取决于连接问题的原因&#xff0c;但始终包括文本“错误651”。 虽然很烦人&#xff0c;但错误651是一个相对较小的问题&#xff0c;不应该导致计算…