yo!这里是STL::list类简单模拟实现

目录

前言

重要接口实现

框架

默认成员函数

迭代器(重点)

1.引言

2.list迭代器类实现 

3.list类中调用实现 

增删查改

后记


前言

        我们知道,stl中的vector对应数据结构中的顺序表,string类对应字符串,而今天要讲的list类对应带头双向链表,并不是对应单链表,带头双向链表的基本操作在数据结构课程中已经学过,所以今天即将要讲的常见接口并不是重点,重点是list的迭代器的实现。

        我们也知道,string、vector的迭代器就是原生指针,如果使用原生指针可以实现list的迭代器吗?答案是不行,因为list的数据并不是一块连续的存储空间,无法像指针那样取访问元素,但是为了保持所有容器迭代器的使用一致,我们如何实现list的迭代器才能像原生指针那样通过++、--去控制,这里就体现出了封装的重要性,快往下看看吧!

重要接口实现

  • 框架

        通过下方代码可以看出,实现了一个节点类模板存储节点,一个链表类模板存储链表,

①使用类模板是因为存储元素可以自由指定,不是像以前一样通过typedef固定了每个链表的元素类型;

②节点类模板使用struct,而不使用class,是因为struct的默认权限是public,链表类模板可以自由访问其成员变量,而class默认权限是private,当然用class指定public权限也可以,

节点类模板中通过构造函数初始化元素,链表类模板成员是一个节点指针,可以在构造函数中申请一个节点充当头节点。

代码:

template <class T>
struct ListNode
{//构造函数用来创节点ListNode(const T& x = T()):_data(x), _prev(nullptr), _next(nullptr){}T _data;ListNode<T>* _prev;ListNode<T>* _next;
};template <class T>
class List
{typedef ListNode<T> Lnode;  //作用:下面写ListNode<T>较麻烦,typedef一下,使用Lnode较方便public://...private:Lnode* _head;
};
  • 默认成员函数

        因为在所有构造函数前都需要先初始化成员变量(为头节点申请空间并将左右指针置空),所以封装了一个函数empty_init在每个构造函数前直接调用,

        所有默认成员函数的实现与string、vector中的如出一辙,不太理解的可以参考一下之前的文章,不是重点这里不再赘述。

代码:

	//创建并初始化头节点,放于构造函数的最前面void empty_init(){_head = new Lnode();_head->_next = _head->_prev = _head;}//构造函数List(){empty_init();}//普通拷贝构造函数List(const List& L)   //注意:类外必须是List<类型名>,类里可以是List,但建议List<T>{empty_init();auto it = L.begin();while (it != L.end()){push_back(*it);it++;}}void swap(List<T>& L){std::swap(_head, L._head);}//传迭代器区间构造函数template <class InputIterator>  List(InputIterator first, InputIterator last){empty_init();while (first != last){push_back(*first);++first;}}//拷贝构造函数//现代写法List(const List<T>& L){empty_init();List<T> tmp(L.begin(), L.end());swap(tmp);}//赋值运算符重载//现代写法List<T>& operator=(const List<T>& L){List<T> tmp(L.begin(), L.end());swap(tmp);return *this;}//更狠的现代写法List<T>& operator=(List<T> L)   //直接传一个拷贝过来,相当于上面的tmp,函数结束自动释放{swap(L);return *this;}//清除除了头节点之外所有的节点void clear(){auto it = begin();while (it != end()){it = erase(it);}}//析构函数~List(){clear();_head->_next = _head->_prev = nullptr;delete _head;_head = nullptr;}
  • 迭代器(重点)

1.引言

        还记得vector、string的迭代器实现吗?typedef T* iterator;   typedef const T* const_iterator;   ,只是原生指针对不对,然后typedef一下,就可以使用了,因为指针可以在一块连续的地址空间++或--访问元素,但是链表中的节点指针++、--访问元素可以吗,答案是不可以,但为了保持迭代器使用一致,就应该遇到链表的迭代器使用++、--也可以达到一样的效果,因此就想到了操作符重载,将++、--、*  重载成我们希望达到的效果,而实现操作符重载就必须将其封装成一个类。

        从引言中就可以看出list与之前学过的容器迭代器实现的不同之处,list迭代器是通过封装成类实现,但迭代器有两种(暂时先不提反向迭代器),一种普通迭代器,一种const迭代器,两种迭代器的实现应该大致内容都一样,小部分不一样(比如,const迭代器的解引用应该返回const不可类型的变量),那我们应该先写好普通迭代器的实现,再复制粘贴成const迭代器然后修修改改吗?漏!大漏特漏!接触过模板这个概念之后,应该可以想到这里用到模板。

2.list迭代器类实现 

1)框架 

        见下方代码, 实现list的迭代器这个__list_iterator类模板,参数列表中的T、Ref、Ptr分别是数据类型、此类型的引用、此类型的指针(比如,T是int,Ref就是int&,Ptr就是int*)(为什么需要指针参数在操作符->重载处有说明),填入的参数不同就是不同的类,这里list的迭代器需要两个类,一个普通迭代器的类,一个const迭代器的类,在list类实现中去定义即可。

        针对list迭代器类模板的实现,成员变量是节点的指针,而构造函数则是传入一个节点指针可初始化一个list迭代器,不需要自己提供析构函数。

代码:

template <class T, class Ref, class Ptr>
struct __list_iterator
{//注意:这两个typedef只是因为ListNode<T>、__list_iterator<T, Ref, Ptr>很麻烦写,所以简化一下,也方便理解typedef ListNode<T> Lnode;typedef __list_iterator<T, Ref, Ptr> iterator;//构造函数__list_iterator(Lnode* p):_node(p){}//...Lnode* _node;   //链表中的迭代器表示节点的指针
};

2) 关系运算符重载

        判断两个迭代器相不相等即判断作为成员变量的结点指针是不是同一个指针变量,很容易理解,加上const是无论普通list对象还是const list对象都可以调用。

代码:

    bool operator==(const iterator& it) const{return _node == it._node;}bool operator!=(const iterator& it) const{return !(*this == it);}

 3)运算符++、--重载

        针对list类,迭代器的++就是访问下一个节点的迭代器,--是访问上一个节点的迭代器,再注意好前置与后置的实现即可,不是很难。

代码:

    iterator& operator++(){_node = _node->_next;return *this;}iterator operator++(int){iterator tmp(_node);_node = _node->_next;return tmp;}iterator& operator--(){_node = _node->_prev;return *this;}iterator operator--(int){iterator tmp(_node);_node = _node->_prev;return tmp;}

4)操作符*重载

        list类中的迭代器解引用就是访问此节点的数据,并且返回引用类型,即可操作其中的值,普通迭代器的Ref是普通引用类型,即可读可写,const迭代器的Ref是const引用类型,只可读不可写。

        注意:不需要将此成员函数设置成const类型,就像本作者初学时所疑惑的,如果Ref是const T&,不应该对应const成员函数(Ref operator*() const)吗?其实不然,list类的迭代器使用了类模板,参数不同就是不同的类,直接将两种迭代器分开了,如果是普通迭代器,Ref就会传进来T&,调用解引用重载时返回引用,可读可写,如果是const迭代器,Ref就会传进来const T&,调用解引用重载时返回const引用,只可读不可写。

代码:

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

 5)操作符->重载

        正常情况下,操作符->可以解引用结构体指针再取其成员,那对于底层是节点指针的list迭代器,->就是对迭代器解引用再取其成员,所以针对_data如果是个自定义类型,那么->就可以取其成员,比如,_data是个自定义类型POS,有两个成员,一个x,一个y,那么it->x,it->y表示迭代器取的POS中的x、y,如下图测试,

        仔细观察->操作符重载的实现,it->x应该写成it->->x才是对的,因为it->返回自定义类型指针,再->x返回其成员,这里其实编译器做了处理,省略了一个->,提高了可读性。

        这里的Ptr就是数据类型的指针变量,将其地址返回,与引用形式一样,需要从类模板参数列表输入。

代码:

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

 测试:

3.list类中调用实现 

        在实现完__list_iterator这个list类迭代器类模板之后,通过在list类中输入不同的模板参数定义不同的类,这里需要普通迭代器类和const迭代器类,如下代码,begin()是返回首元节点的迭代器,而end()是返回最后一个元素节点的下一个位置迭代器,即头节点迭代器。

 代码:

    typedef __list_iterator<T, T&, T*> iterator;  //普通迭代器类typedef __list_iterator<T, const T&, const T*> const_iterator;   //const迭代器类iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}
  • 增删查改

        在理解了list的迭代器实现之后,对list的增删改查想必是游刃有余,这里实现一下基本的插入删除操作,结合之前在数据结构中的知识,insert、erase的实现应该可以很快写出,值得注意的是,insert返回新插入节点的迭代器,erase返回所删节点的下一位置的迭代器,而尾插、尾删、头插、头删直接复用即可。

代码:

	iterator insert(iterator pos, const T& x){Lnode* newNode = new Lnode(x);pos._node->_prev->_next = newNode;newNode->_prev = pos._node->_prev;newNode->_next = pos._node;pos._node->_prev = newNode;return iterator(newNode);  //返回插入位置的迭代器}//尾插void push_back(const T& x){insert(end(), x);}//头插void push_front(const T& x){insert(begin(), x);}iterator erase(iterator pos){assert(pos != end());Lnode* tmp = pos._node->_next;pos._node->_prev->_next = pos._node->_next;pos._node->_next->_prev = pos._node->_prev;delete pos._node;return iterator(tmp);}//尾删void pop_back(){erase(--end());}//头删void pop_front(){erase(begin());}

后记

        在list类的实现中,默认成员函数、操作符重载以及增删查改已不再是重点,重点是掌握迭代器的实现,因为与string、vector中的迭代器实现不同,也没有那么简单,上面总结的很清楚,不懂的可以私我或者写在评论区,加油,拜拜!


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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

时序预测 | MATLAB实现CNN-BiGRU-Attention时间序列预测

时序预测 | MATLAB实现CNN-BiGRU-Attention时间序列预测 目录 时序预测 | MATLAB实现CNN-BiGRU-Attention时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MATLAB实现CNN-BiGRU-Attention时间序列预测&#xff0c;CNN-BiGRU-Attention结合注意力机制时…

Streamlit项目: 轻松搭建部署个人博客网站

文章目录 1 前言1.1 探索 Streamlit&#xff1a;轻松创建交互式应用1.2 最全 Streamlit 教程专栏 2 我的个人博客网站已上线&#xff01;2.1 一个集成了智能中医舌诊-中e诊专栏的博客网站2.2 前期准备2.3 使用 Streamlit Cloud 运行 3 知识点讲解3.1 实现多页面&#xff1a;两种…

KMP算法比较次数

主串T “abaabaabcabaabc”&#xff0c;模式串S“abaabc”&#xff0c;采用KMP算法匹配&#xff0c;到匹配成功为止&#xff0c;比较次数是&#xff1a; 序号&#xff1a; 1 2 3 4 5 6 模式串&#xff1a;a b a a b c next[j]&#xff1a; 0 1 1 2 2 3 关于next数组求法&#…

Tomcat的多实例和动静分离

目录 一、多实例 二、 nginxtomcat的负载均衡和动静分离 三、Tomcat 客户端->四层代理->七层代理->tomcat服务器 实验&#xff1a; 问题总结&#xff1a; tomcat日志文件&#xff1a;/usr/local/tomcat/logs/catalina.out 一、多实例 在一台服务器上有多个tomc…

如何在电力行业运用IPD?

电力行业是国民经济众多垄断行业中较早实施改革的行业之一。近几年我国电力行业保持着较快的发展速度&#xff0c;也取得了很大的成绩&#xff0c;发电机容量和发电量居世界首位。2015-2020年&#xff0c;全国发电量不断攀升。 电力是以电能作为动力的能源。电力的发现和应用掀…

【BASH】回顾与知识点梳理(十四)

【BASH】回顾与知识点梳理 十四 十四. 文件与目录的默认权限与隐藏权限14.1 文件预设权限&#xff1a;umaskumask 的利用与重要性&#xff1a;专题制作 14.2 文件隐藏属性chattr (配置文件案隐藏属性)lsattr (显示文件隐藏属性) 14.3 文件特殊权限&#xff1a; SUID, SGID, SBI…

打造企业或者个人IP引流法

打造企业或者个人IP引流法. 大家好&#xff0c;我是百收网SEO编辑&#xff1a;狂潮老师&#xff0c;今天给大家分享企业IP打造的方法 首先我们想让人知道你的企业叫什么&#xff0c;怎么找到你的企业 这个时候我们就需要去各大平台发布信息&#xff0c;客户想了解直接去搜索…

Torch基本操作扫盲

torch.rand是均匀分布采样 torch.randn是标准正态分布采样 同时设定好了GPU种子 高斯/正态分布

Android多屏幕支持-Android12

Android多屏幕支持-Android12 1、概览及相关文章2、屏幕窗口配置2.1 配置xml文件2.2 DisplayInfo#uniqueId 屏幕标识2.3 adb查看信息 3、配置文件解析3.1 xml字段读取3.2 简要时序图 4、每屏幕焦点 android12-release 1、概览及相关文章 AOSP > 文档 > 心主题 > 多屏…

Rabbitmq消息不丢失

目录 一、消息不丢失1.消息确认2.消息确认业务封装2.1 发送确认消息测试2.2 消息发送失败&#xff0c;设置重发机制 一、消息不丢失 消息的不丢失&#xff0c;在MQ角度考虑&#xff0c;一般有三种途径&#xff1a; 1&#xff0c;生产者不丢数据 2&#xff0c;MQ服务器不丢数据…

开源数据库Mysql_DBA运维实战 (DDL语句)

DDL DDL语句 数据库定义语言&#xff1a;数据库、表、视图、索引、存储过程. 例如:CREATE DROP ALTER DDL库 定义库{ 创建业务数据库&#xff1a;CREAATE DATABASE ___数据库名___ ; 数据库名要求{ a.区分大小写 b.唯一性 c.不能使用关键字如 create select d.不能单独使用…

最强自动化测试框架Playwright(22)-模拟器

可以使用测试生成器通过仿真生成测试&#xff0c;以便为特定窗口、设备、配色方案生成测试&#xff0c;以及模拟地理位置、语言或时区。测试生成器还可以生成测试&#xff0c;同时保留经过身份验证的状态。 模拟视口大小 Playwright 打开一个浏览器窗口&#xff0c;其视口设置…