当前位置: 首页 > news >正文

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;}

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

相关文章:

C++中List的实现

前言 数据结构中&#xff0c;我们了解到了链表&#xff0c;但是我们使用时需要自己去实现链表才能用&#xff0c;但是C出现了list将这一切皆变为现。list可以看作是一个带头双向循环的链表结构&#xff0c;并且可以在任意的正确范围内进行增删查改数据的容器。list容器一样也是…...

ElementUI 树形表格的使用以及表单嵌套树形表格的校验问题等汇总

目录 一、树形表格如何添加序号体现层级关系 二、树形表格展开收缩图标位置放置&#xff0c;设置指定列 三、表单嵌套树形表格的校验问题以及如何给校验rules传参 普通表格绑定如下&#xff1a;这种方法只能校验表格的第一层&#xff0c;树形需要递归设置子级节点prop。 树…...

解决“Unable to start embedded Tomcat“错误的完整指南

系列文章目录 文章目录 系列文章目录前言一、查看错误信息二、确认端口是否被占用三、检查依赖版本兼容性四、清理临时文件夹五、检查应用程序配置六、检查依赖冲突七、查看异常堆栈信息八、升级或降级Spring Boot版本总结前言 在使用Spring Boot开发应用程序时,有时可能会遇…...

JVS开源基础框架:平台基本信息介绍

JVS是面向软件开发团队可以快速实现应用的基础开发脚手架&#xff0c;主要定位于企业信息化通用底座&#xff0c;采用微服务分布式框架&#xff0c;提供丰富的基础功能&#xff0c;集成众多业务引擎&#xff0c;它灵活性强&#xff0c;界面化配置对开发者友好&#xff0c;底层容…...

C++ - max_element

在C中&#xff0c;要找到一个数组中的最大元素&#xff0c;可以使用 std::max_element 函数。以下是使用步骤&#xff1a; 包含 <algorithm> 头文件&#xff0c;这里定义了 std::max_element 函数。声明一个数组&#xff0c;并初始化它。使用 std::max_element 函数来查找…...

聚隆转债上市价格预测

聚隆转债 基本信息 转债名称&#xff1a;聚隆转债&#xff0c;评级&#xff1a;A&#xff0c;发行规模&#xff1a;2.185亿元。 正股名称&#xff1a;南京聚隆&#xff0c;今日收盘价&#xff1a;16.64元&#xff0c;转股价格&#xff1a;18.27元。 当前转股价值 转债面值 / 转…...

pytest自动生成测试类 demo

一、 pytest自动生成测试类 demo # -*- coding:utf-8 -*- # Author: 喵酱 # time: 2023 - 08 -15 # File: test4.py # desc: import pytest import unittest# 动态生成测试类def create_test_class(class_name:str, test_cases:list) -> type:"""生成测试类…...

服务器卡顿了该如何处理

服务器卡顿了该如何处理 当Windows系统的服务器出现卡顿问题时&#xff0c;以下是一些常见的故障排除步骤&#xff1a; 1.检查网络连接&#xff1a;确保服务器的网络连接正常。检查网络设备、交换机、防火墙等设备&#xff0c;确保它们正常运行。尝试通过其他计算机访问服务器…...

常量对象 只能调用 常成员函数

一、遇到问题&#xff1a; //函数声明 void ReadRanFile(CString szFilePath); const CFvArray<CString>& GetPanelGrade() const { return m_fvArrayPanelGrade; } //在另一个文件中调用ReadtRanFile这个函数 const CFsJudConfig& psJudConfig m_pFsDefJu…...

Progressive-Hint Prompting Improves Reasoning in Large Language Models

本文是LLM系列的文章&#xff0c;针对《Progressive-Hint Prompting Improves Reasoning in Large Language Models》的翻译。 渐进提示改进了大型语言模型中的推理 摘要1 引言2 相关工作3 渐进提示Prompting4 实验5 结论6 实现细节7 不足与未来工作8 广泛的影响9 具有不同提示…...

mysql中INSERT INTO ... ON DUPLICATE KEY UPDATE的用法,以及与REPLACE INTO 语句用法的异同

INSERT INTO ... ON DUPLICATE KEY UPDATE 是 MySQL 中一种用于插入数据并处理重复键冲突的语法。与之相似的还有 REPLACE INTO 语句。以下是它们的用法和异同点的详细说明&#xff1a; 一、INSERT INTO ... ON DUPLICATE KEY UPDATE INSERT INTO ... ON DUPLICATE KEY UPDAT…...

wireshark 实用过滤表达式(针对ip、协议、端口、长度和内容)

wireshark 实用过滤表达式&#xff08;针对ip、协议、端口、长度和内容&#xff09; 1. 关键字 “与”&#xff1a;“eq” 和 “”等同&#xff0c;可以使用 “and” 表示并且&#xff0c; “或”&#xff1a;“or”表示或者。 “非”&#xff1a;“!" 和 "not”…...

MATLAB图形窗口固定

起因是上次作图的时候写了&#xff1a; clc clear close all 这三个典型的刷新语句 清空工作区、命令行并且关闭图窗 就导致每次我把图窗拉到合适的位置观察&#xff0c;再一次点击运行都会重新刷新在出生点&#xff08;x&#xff09; 所以想把图窗固定在某个位置 显然更…...

【数据结构】_7.二叉树概念与基本操作

目录 1.树形结构 1.1 树的概念 1.2 树的相关概念 1.3 树的表示 1.4 树在实际中的应用—表示文件系统的目录树结构 ​编辑​2.二叉树 2.1 概念 2.2 特殊二叉树 2.3 二叉树的性质 2.4 二叉树的存储结构 2.4.1 顺序存储结构&#xff08;数组存储结构&#xff09; 2.4.2…...

Flink之Partitioner(分区规则)

Flink之Partitioner(分区规则) 方法注释global()全部发往1个taskbroadcast()广播(前面的文章讲解过,这里不做阐述)forward()上下游并行度一致时一对一发送,和同一个算子连中算子的OneToOne是一回事shuffle()随机分配(只是随机,同Spark的shuffle不同)rebalance()轮询分配,默认机…...

tk切换到mac的code分享

文章目录 前言一、基础环境配置二、开发软件与扩展1.用到的开发软件与平替、扩展情况 总结 前言 最近换上了coding人生的第一台mac&#xff0c;以前一直偏好tk&#xff0c;近来身边的朋友越来越多的用mac了&#xff0c;win的自动更新越来越占磁盘了&#xff0c;而且win11抛弃了…...

spark的standalone 分布式搭建

一、环境准备 集群环境hadoop11&#xff0c;hadoop12 &#xff0c;hadoop13 安装 zookeeper 和 HDFS 1、启动zookeeper -- 启动zookeeper(11,12,13都需要启动) xcall.sh zkServer.sh start -- 或者 zk.sh start -- xcall.sh 和zk.sh都是自己写的脚本-- 查看进程 jps -- 有…...

浅析基于视频汇聚与AI智能分析的新零售方案设计

一、行业背景 近年来&#xff0c;随着新零售概念的提出&#xff0c;国内外各大企业纷纷布局智慧零售领域。从无人便利店、智能售货机&#xff0c;到线上线下融合的电商平台&#xff0c;再到通过大数据分析实现精准推送的个性化营销&#xff0c;智慧零售的触角已经深入各个零售…...

SpringMVC之异常处理

SpringMVC之异常处理 异常分为编译时异常和运行时异常&#xff0c;编译时异常我们trycatch捕获&#xff0c;捕获后自行处理&#xff0c;而运行时异常是不可预期的&#xff0c;就需要规范编码来避免&#xff0c;在SpringMVC中&#xff0c;不管是编译异常还是运行时异常&#xff…...

保险龙头科技进化论:太保的六年

如果从2013年中国首家互联网保险公司——众安在线的成立算起&#xff0c;保险科技在我国的发展已走进第十个年头。十年以来&#xff0c;在政策指引、技术发展和金融机构数字化转型的大背景下&#xff0c;科技赋能保险业高质量发展转型已成为行业共识。 大数据、云计算、人工智…...

升级STM32电机PID速度闭环编程:从F1到F4的移植技巧与实例解析

引言&#xff1a; 在嵌入式系统开发中&#xff0c;STM32系列微控制器广泛应用于各种应用领域。而对于直流有刷电机的控制&#xff0c;PID速度闭环是一种常用的控制方式。本文将以此为例&#xff0c;探讨如何从STM32F1系列移植到STM32F4系列&#xff0c;并详细介绍HAL库在不同型…...

GaussDB 实验篇+openGauss的4种1级分区案例

✔ 范围分区/range分区 -- 创建表 drop table if exists zzt.par_range; create table if not exists zzt.par_range (empno integer,ename char(10),job char(9),mgr integer(4),hiredate date,sal numeric(7,2),comm numeric(7,2),deptno integer,constraint pk_par_emp pri…...

Ruby软件外包开发语言特点

Ruby 是一种动态、开放源代码的编程语言&#xff0c;它注重简洁性和开发人员的幸福感。在许多方面都具有优点&#xff0c;但由于其动态类型和解释执行的特性&#xff0c;它可能不适合某些对性能和类型安全性要求较高的场景。下面和大家分享 Ruby 语言的一些主要特点以及适用的场…...

《系统架构设计师教程》重点章节思维导图

内容来自《系统架构设计师教程》&#xff0c;筛选系统架构设计师考试中分值重点分布的章节&#xff0c;根据章节的内容整理出相关思维导图。 重点章节 第2章&#xff1a;计算机系统知识第5章&#xff1a;软件工程基础知识第7章&#xff1a;系统架构设计基础知识第8章&#xff1…...

mac录屏工具,录屏没有声音的解决办法

mac录屏工具&#xff0c;录屏没有声音的解决办法 在使用macbook录制屏幕时&#xff0c;发现自带的录屏工具QuickTime Player没有声音&#xff0c;于是尝试了多款录屏工具&#xff0c;对其做一些经验总结&#xff08;省流&#xff1a;APP Store直接可以免费下载使用Omi录屏专家…...

神经网络基础-神经网络补充概念-33-偏差与方差

概念 偏差&#xff08;Bias&#xff09;&#xff1a; 偏差是模型预测值与实际值之间的差距&#xff0c;它反映了模型对训练数据的拟合能力。高偏差意味着模型无法很好地拟合训练数据&#xff0c;通常会导致欠拟合。欠拟合是指模型过于简单&#xff0c;不能捕捉数据中的复杂模式…...

单片机第一季:零基础13——AD和DA转换

1&#xff0c;AD转换基本概念 51 单片机系统内部运算时用的全部是数字量&#xff0c;即0 和1&#xff0c;因此对单片机系统而言&#xff0c;无法直接操作模拟量&#xff0c;必须将模拟量转换成数字量。所谓数字量&#xff0c;就是用一系列0 和1 组成的二进制代码表示某个信号大…...

小区外卖跑腿,解决最后100米配送难题

小区外卖跑腿&#xff0c;解决最后100米配送难题 小区外卖跑腿作为新市场环境下的创业模式&#xff0c;通过选择小区里的闲散人员作为骑手&#xff0c;解决了最后100米配送的问题。这项业务不仅包括小区业主的取快递、寄快递等日常需求&#xff0c;还能提供小区帮忙、小区外卖…...

ZooKeeper的应用场景(命名服务、分布式协调通知)

3 命名服务 命名服务(NameService)也是分布式系统中比较常见的一类场景&#xff0c;在《Java网络高级编程》一书中提到&#xff0c;命名服务是分布式系统最基本的公共服务之一。在分布式系统中&#xff0c;被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等一这…...

网络套接字

网络套接字 文章目录 网络套接字认识端口号初识TCP协议初识UDP协议网络字节序 socket编程接口socket创建socket文件描述符bind绑定端口号sockaddr结构体netstat -nuap&#xff1a;查看服务器网络信息 代码编译运行展示 实现简单UDP服务器开发 认识端口号 端口号(port)是传输层协…...