C++STL(六)——list模拟

目录

  • 本次所需实现的三个类
  • 一、结点类的模拟实现
    • 构造函数
  • 二、迭代器类的模拟实现
    • 为什么有迭代器类
    • 迭代器类的模板参数说明
    • 构造函数
    • ++运算符的重载
    • - -运算符的重载
    • ==和!=运算符的重载
    • *运算符的重载
    • ->运算符的重载
      • 引入模板第二个和第三个参数
  • 三、list的模拟实现
    • 3.1 默认成员函数
      • 构造函数
      • 拷贝构造函数
      • 赋值运算符重载函数
      • 析构函数
    • 3.2 迭代器相关函数
      • begin和end
    • 3.3 访问容器相关函数
      • front和back
    • 3.4 插入、删除函数
      • insert和erase
      • push_back和pop_back
      • push_front和pop_front
    • 3.5 其他函数
      • size
      • clear
      • empty
      • swap
  • 总结


本次所需实现的三个类

一、结点类的模拟实现

list类是由结点类和迭代器类组成

我们经常说list在底层实现时就是一个链表,更准确来说,list实际上是一个带头双向循环链表。
在这里插入图片描述

因此,我们若要实现list,则首先需要实现一个结点类。而一个结点需要存储的信息有:数据、前一个结点的地址、后一个结点的地址,于是该结点类的成员变量也就出来了(数据、前驱指针、后继指针)。

而对于该结点类的成员函数来说,我们只需实现一个构造函数即可。因为该结点类只需要根据数据来构造一个结点即可,而结点的释放则由list的析构函数来完成。

构造函数

template<class T>				//由于该list可能是任何类型则使用模板类
struct list_node				//存放结点成员变量
{
	list_node<T>* _prev;
	list_node<T>* _next;
	T _val;
	
	list_node(const T& val = T())		//缺省值初始化,T不一定是内置类型所以不能直接给0
		:_prev(nullptr)
		, _next(nullptr)
		, _val(val)
	{}
};

注意: 若构造结点时没有传入数据,则默认以list容器所存储类型的默认构造函数所构造出来的值为传入数据。

二、迭代器类的模拟实现

引入:

list迭代器是一个自定义类型,内部成员是结点指针(内置类型),我们本身想要的就是结点指针,结点指针就可以做迭代器,它不能像原生指针(如:vector、string)一样地址是连续的而list地址不是,因为底层结构的差异,所以用一个类封装结点指针,然后重载运算符后就可以像内置类型一样访问。

为什么有迭代器类

在学习string和vector时都没有说专门要实现一个迭代器类,为什么实现list的时候就需要实现一个迭代器类呢?

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

在这里插入图片描述

但是对于list来说,其各节点在内存中位置可能不都是连续的,大多情况下都是随机的,我们不能仅通过结点指针的自增、自减以及解引用等操作对相应结点的数据进行操作,需找到下一个结点才能访问。
在这里插入图片描述迭代器的意义是让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问。

既然list的结点指针的行为不满足迭代器定义,那么我们可以对这个结点指针进行封装,对结点指针的各种运算符操作进行重载,使得我们可以用和string和vector当中的迭代器一样的方式使用list当中的迭代器。比如使用list当中的迭代器进行自增操作时,实际上执行了node = node->next语句。

list迭代器类实际上就是对结点指针进行了封装,对其各种运算符进行了重载,使得结点指针的各种行为在使用者角度看起来和普通指针一样。

迭代器类的模板参数说明

list迭代器类的模板参数列表当中有三个模板参数

template<class T, class Ref, class Ptr>

在list的模拟实现当中,我们typedef重命名了两个迭代器类型,普通迭代器和const迭代器。

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

迭代器类的模板参数列表当中的Ref和Ptr分别代表的是引用类型和指针类型。

构造函数

迭代器类实际上就是对结点指针进行了封装,其成员变量就只有一个,那就是结点指针,其构造函数直接根据所给结点指针构造一个迭代器对象。

Node* _node;

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

++运算符的重载

当然也分为前置和后置++

self& operator++()			//返回类型还是迭代器
{
	_node = _node->_next;					//下一个位置
	return *this;
}

self operator++(int)
{
	self<T> 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;
}

==和!=运算符的重载

当使用==运算符比较两个迭代器时,实际上想知道的是这两个迭代器是否是同一个位置的迭代器,判断这两个迭代器当中的结点指针的指向是否相同即可。!=运算符则相反。

//通过结点的指针比较,两数据比较时end返回的数据具有常性要加const
bool operator!=(const self& it)
{
	return _node != it._node;
}
bool operator==(const self& it)
{
	return _node == it._node;
}

*运算符的重载

使用解引用操作符时,是想得到该位置的数据内容。因此,直接返回当前结点指针所指结点的数据即可,但是这里需要使用引用返回,因为解引用后可能需要对数据进行修改。

Ref operator*()								//出了作用域还在可以引用返回
{
	return _node->_val;						//结点指针的数据
}

->运算符的重载

有些情景下我们使用迭代器的时候可能会用到->运算符。

void test2()
{
	struct A
	{
		A(int a=0,int b=0)
			:_a(a)
			,_b(b)
		{}

		int _a;
		int _b;
	};
	ling::list<A> lt;
	lt.push_back(A(1,1));
	lt.push_back(A(2,2));
	lt.push_back(A(3,3));
	lt.push_back(A(4,4));
	ling::list<A>::iterator it = lt.begin();
	while (it != lt.end())
	{
		//cout << *it << " ";					//遍历对象是自定义类型要重载流插入才可以打印

		//都能实现遍历
		//cout << (*it)._a << " "<<(*it)._b << endl;	
		cout << it->_a << " " << it->_b << endl;
		++it;
	}
	cout << endl;
}

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

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

引入模板第二个和第三个参数

如上例子:
在这里插入图片描述

三、list的模拟实现

3.1 默认成员函数

list是一个带头双向循环链表,在构造一个list对象时,申请一个头结点,并让其前驱指针和后继指针都指向自己
在这里插入图片描述
由于有时容易忘记写上显示实例化模板参数才构成类型

typedef list_node<T> node;				//重命名一下

构造函数

list()							//初始化构成双链表
{
	_head = new Node;			//给头节点申请空间
	head->_prev = head;			//前后指针指向自己
	head->_next = head;
}

拷贝构造函数

拷贝构造函数就是根据所给list容器,拷贝构造出一个对象。对于拷贝构造函数,先申请一个头结点,并让其前驱指针和后继指针都指向自己,然后将所给容器当中的数据,通过遍历的方式一个个尾插到新构造的容器后面。

//拷贝构造函数
list(const list<T>& lt)
{
	_head = new node; 		//申请一个头结点
	_head->_next = _head; 	//头结点的后继指针指向自己
	_head->_prev = _head; 	//头结点的前驱指针指向自己
	for (const auto& e : lt)			//记得引用
	{
		push_back(e);		 //将容器lt当中的数据一个个尾插到新构造的容器后面
	}
}

赋值运算符重载函数

一般两种写法

//传统写法
list<T>& operator=(const list<T>& lt)
{
	if (this != &lt) 			//避免自己给自己赋值
	{
		clear(); 				//清空容器
		for (const auto& e : lt)
		{
			push_back(e); 		//将容器lt当中的数据一个个尾插到链表后面
		}
	}
	return *this; 				//支持连续赋值
}

//现代写法
list<T>& operator=(list<T> lt) //编译器接收右值的时候自动调用其拷贝构造函数
{
	swap(lt); 					//交换这两个对象
	return *this; 				//支持连续赋值
}

析构函数

对象进行析构时,首先调用clear函数清理容器当中的数据,然后将头结点释放,最后将头指针置空

//析构函数
~list()
{
	clear(); 			//清理容器
	delete _head; 		//释放头结点
	_head = nullptr; 	//头指针置空
}

3.2 迭代器相关函数

begin和end

begin函数返回的是第一个有效数据的迭代器,end函数返回的是最后一个有效数据的下一个位置的迭代器。

list带头双向循环链表,第一个有效数据的迭代器就是使用头结点的下一个结点的地址构造出来的迭代器,最后一个有效数据的下一个位置的迭代器就是使用头结点的地址构造出来的迭代器。(最后一个结点的下一个结点就是头结点)

//单参数的构造函数支持隐式类型转换,两种写法都可以都是生成匿名对象
iterator begin()					
{
	return _head->_next;
	//return iterator(_head->_next);
}
iterator end()
{
	return _head;
	//return iterator(_head);
}

3.3 访问容器相关函数

front和back

分别用于获取第一个有效数据和最后一个有效数据,因此,实现front和back函数时,直接返回第一个有效数据和最后一个有效数据的引用即可。

T& front()
{
	return *begin(); 		//返回第一个有效数据的引用
}
T& back()
{
	return *(--end()); 		//返回最后一个有效数据的引用
}

//不可修改
const T& front() const
{
	return *begin(); 		//返回第一个有效数据的const引用
}
const T& back() const
{
	return *(--end()); 		//返回最后一个有效数据的const引用
}

3.4 插入、删除函数

insert和erase

当然还是任意位置插入和删除,由于会迭代器失效所以有返回值

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

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

//删除
iterator erase(iterator pos)
{
	assert(pos != end());
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

	prev->_next = next;
	next->_prev = prev;
	delete cur;
	return next;
}

有了这对增删函数就可以复用在其它增删的函数身上了

push_back和pop_back

分别对应尾插、尾删

//尾插
void push_back(const T& x)
{
	Node* tail = _head->prev;				//找到尾
	Node* newnode = new Node(x);			//调用结点的构造函数

	//更新尾结点
	tail->_next = newnode;
	newnode->_prev = tail;
	_head->_prev = newnode;
	newnode->_next = _head;

	//可直接复用insert
	insert(end(),x);
}

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

push_front和pop_front

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

//头删
void pop_front()
{
	erase(begin());
}

3.5 其他函数

size

获取当前容器当中的有效数据个数,因为list是链表,所以只能通过遍历的方式逐个统计有效数据的个数。

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

clear

用于清空容器,通过遍历的方式,逐个删除结点,只保留头结点

void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

empty

用于判断容器是否为空,直接判断该容器的begin函数和end函数所返回的迭代器,是否是同一个位置的迭代器即可。(此时说明容器当中只有一个头结点)

bool empty() const
{
	return begin() == end(); //判断是否只有头结点
}

swap

用于交换两个容器,list容器当中存储的实际上就只有链表的头指针,将这两个容器当中的头指针交换即可。

void swap(list<T>& lt)
{
	std::swap(_head, lt._head); 		//交换两个容器当中的头指针即可
}

总结

由于list类不一定只接收一个类型如:内置类型(int、double),自定义类型,所以三个类都使用了模板。
类名+模板参数才构成类型容易忘记,往往typedef重命名一下它们也更方便使用。
在这里插入图片描述

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

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

相关文章

国产编辑器EverEdit - 替换功能详解

1 替换 1.1 应用场景 替换文本是在文档编辑过程中不可回避的操作&#xff0c;是将指定的关键词替换为新的文本&#xff0c;比如&#xff1a;写代码时修改变量名等。 1.2 使用方法 1.2.1 基本替换 使用主菜单查找 -> 替换&#xff0c;或使用快捷键Ctrl H&#xff0c;会打…

LIMO:上海交大的工作 “少即是多” LLM 推理

25年2月来自上海交大、SII 和 GAIR 的论文“LIMO: Less is More for Reasoning”。 一个挑战是在大语言模型&#xff08;LLM&#xff09;中的复杂推理。虽然传统观点认为复杂的推理任务需要大量的训练数据&#xff08;通常超过 100,000 个示例&#xff09;&#xff0c;但本文展…

防御保护作业二

拓扑图 需求 需求一&#xff1a; 需求二&#xff1a; 需求三&#xff1a; 需求四&#xff1a; 需求五&#xff1a; 需求六&#xff1a; 需求七&#xff1a; 需求分析 1.按照要求进行设备IP地址的配置 2.在FW上开启DHCP功能&#xff0c;并配置不同的全局地址池&#xff0c;为…

蓝桥与力扣刷题(226 翻转二叉树)

题目&#xff1a;给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1]示例 2&#xff1a; 输入&#xff1a;root [2,1,3] 输出&#xff1a;[2,…

大型语言模型(LLM)中的自适应推理预算管理:基于约束策略优化的解决方案

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

[EAI-033] SFT 记忆,RL 泛化,LLM和VLM的消融研究

Paper Card 论文标题&#xff1a;SFT Memorizes, RL Generalizes: A Comparative Study of Foundation Model Post-training 论文作者&#xff1a;Tianzhe Chu, Yuexiang Zhai, Jihan Yang, Shengbang Tong, Saining Xie, Dale Schuurmans, Quoc V. Le, Sergey Levine, Yi Ma 论…

大数据-259 离线数仓 - Griffin架构 修改配置 pom.xml sparkProperties 编译启动

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; Java篇开始了&#xff01; 目前开始更新 MyBatis&#xff0c;一起深入浅出&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff0…

【时时三省】(C语言基础)基础习题1

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 1.什么是程序&#xff1f;什么是程序设计 程序是为实现特定目标或解决特定问题&#xff0c;用计算机能理解和执行的语言编写的一系列指令的集合。 程序设计是问题分析&#xff0c;设计算法…

防火墙用户认证实验

1、创建vlan10和vlan20 2、将接口划分到对应的vlan中 [FW]interface GigabitEthernet 1/0/1.1 [FW-GigabitEthernet1/0/1.1]ip address 172.16.1.254 24 [FW-GigabitEthernet1/0/1.1]vlan-type dot1q 10 [FW]interface GigabitEthernet 1/0/1.2 [FW-GigabitEthernet1/0/1.1]ip …

VUE项目中实现权限控制,菜单权限,按钮权限,接口权限,路由权限,操作权限,数据权限实现

VUE项目中实现权限控制&#xff0c;菜单权限&#xff0c;按钮权限&#xff0c;接口权限&#xff0c;路由权限&#xff0c;操作权限&#xff0c;数据权限实现 权限系统分类&#xff08;RBAC&#xff09;引言菜单权限按钮权限接口权限路由权限 菜单权限方案方案一&#xff1a;菜单…

ESXi Host Client创建ubuntu虚拟机教程及NVIDIA显卡驱动安装

参考文章 VMware虚拟机显卡直通记录 AIGC 实战&#xff08;环境篇&#xff09; - EXSI 8.0 Debian安装RTX3060显卡驱动 重点介绍 client版本是7.0.3 注意&#xff1a;下图中不要选择BIOS 按照两个链接中的方法进行操作&#xff0c;以及本章节的上面几个图片的配置之后&a…

DeepSeek帮助做【真】软件需求-而不是批量刷废话

尝试给DeepSeek一份系统用例规约&#xff0c;让它帮判断哪些地方还没有覆盖涉众利益。结果见以下 需求工作的重点可以放在建模精细的真实现状流程和精细的真实涉众利益上&#xff0c;AI帮助推演系统需求。

apache-poi导出excel数据

excel导出 自动设置宽度&#xff0c;设置标题框&#xff0c;设置数据边框。 excel导出 添加依赖 <dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.2</version></dependency>…

10 FastAPI 的自动文档

FastAPI 是一个功能强大且易于使用的 Web 框架&#xff0c;它的最大亮点之一就是内置的 自动文档生成 功能。通过集成 Swagger UI 和 ReDoc&#xff0c;FastAPI 可以自动为我们的 API 生成交互式文档。这不仅使得开发者能够更快速地了解和测试 API&#xff0c;还能够为前端开发…

微软AI研究团队推出LLaVA-Rad:轻量级开源基础模型,助力先进临床放射学报告生成

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

mysql8.0使用MHA实现高可用

一、MHA 介绍 MHA&#xff08;Master HA&#xff09;是一款开源的 MySQL 的高可用程序&#xff0c;它为 MySQL 主从复制架构提供了 automating master failover 功能。MHA 在监控到 master 节点故障时&#xff0c;会提升其中拥有最新数据的 slave 节点成为新的master 节点&…

D3实现站点路线图demo分享

分享通过D3实现的站点路线分布图demo&#xff0c;后续会继续更新其他功能。 功能点 点位弹窗 效果图如下&#xff1a; 轨迹高亮 效果图如下&#xff1a; 添加路线箭头 箭头展示逻辑&#xff1a;根据高速路线最后两个点位&#xff0c;计算得出箭头的点位 效果图如下&#x…

【系统架构设计师】操作系统 ③ ( 存储管理 | 页式存储弊端 - 段式存储引入 | 段式存储 | 段表 | 段表结构 | 逻辑地址 的 合法段地址判断 )

文章目录 一、页式存储弊端 - 段式存储引入1、页式存储弊端 - 内存碎片2、页式存储弊端 - 逻辑结构不匹配3、段式存储引入 二、段式存储 简介1、段式存储2、段表3、段表 结构4、段内地址 / 段内偏移5、段式存储 优缺点6、段式存储 与 页式存储 对比 三、逻辑地址 的 合法段地址…

物联网软件开发与应用方向应该怎样学习,学习哪些内容,就业方向是怎样?(文末领取整套学习视频,课件)物联网硬件开发与嵌入式系统

随着物联网技术的飞速发展&#xff0c;物联网软件开发与应用方向成为了众多开发者关注的焦点。那么&#xff0c;如何在这个领域中脱颖而出呢&#xff1f;本文将为你提供一份详细的学习指南&#xff0c;帮助你从零开始&#xff0c;逐步掌握物联网软件开发与应用的核心技能。 一…

Linux——基础命令1

$&#xff1a;普通用户 #&#xff1a;超级用户 cd 切换目录 cd 目录 &#xff08;进入目录&#xff09; cd ../ &#xff08;返回上一级目录&#xff09; cd ~ &#xff08;切换到当前用户的家目录&#xff09; cd - &#xff08;返回上次目录&#xff09; pwd 输出当前目录…