STL—list—模拟实现【迭代器的实现(重要)】【基本接口的实现】

STL—list—模拟实现

1.list源代码

要想模拟实现list,还是要看一下STL库中的源代码。

image-20240805183908079

_list_node里面装着指向上一个节点的指针prev,和指向下一个节点的指针next,还有数据data

并且它给的是void*,导致后面进行节点指针的返回时需要进行强转

image-20240806164813295

前面的link_type就是节点的指针类型,对(*node).next进行强转。

这样有点麻烦,我自己的模拟实现就不搞这个void*了,直接给节点的指针类型,这样后面不用强转。

2.list模拟实现

为了避免和库里的list发生冲突,我们要自己开辟一个命名空间。名字随意。

首先节点__list_node是一个自定义类型,list是一个带头双向循环链表

__list_node需要存放三个成员变量,存放的就是一个指向上一个节点的指针_prev,一个指向下一个节点的指针_next,还有存放的数据_data

list需要存放一个指向头节点(哨兵位)的指针_head

namespace wzf
{template<class T>struct __list_node{__list_node<T>* _prev;__list_node<T >* _next;T _data;__list_node(const T& x = T()) // 要给缺省值。因为头节点不好给值,用默认的初始值:_next(nullptr),_prev(nullptr),_data(x){}};template<class T>class list // 带头双向循环链表{typedef __list_node<T> Node;public:typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;typedef Reverse_list_iterator<iterator> reverse_iterator;typedef Reverse_list_iterator<const_iterator>const_reverse_iterator;private:Node* _head; };
}

2.1构造函数

list是带头双向循环链表。构造函数的话就开辟一个空间给头节点,然后再让其自身的两个指针指向自己就OK。

		list(){_head = new Node;_head->_next = _head;_head->_prev = _head;}

2.2正向迭代器(重要)

在list的模拟实现中,最重要的就是迭代器的实现。因为他不在是简单的指针了,它有一个自定义类型进行了封装,这个类型里面装的还是节点的指针,通过重载++等运算符去实现迭代器的移动。

为什么要这么做?【因为list是链表,链表的节点不是在物理上不是连续的地址,是一块块的,只是由指针链接到了一起而已。如果直接用节点的指针来做迭代器,在进行++等操作就会出现错误。】

list的迭代器实现:

简单来说就是用一个类型去封装节点的指针,从而构成一个自定义类型。再去重载++等操作符,在重载中去实现节点的迭代

image-20240805185025744

对于实现一个迭代器来说,有两种方法。

  1. 一种是原生指针,就是之前的vector的迭代器
  2. 第二组就是对一个自定义类型进行封装,让它具备指针的功能。

而我们对list的迭代器实现要通过对自定义类型进行封装

因此这个自定义的类,我们要对其进行封装,重载操作符,让其具备基本功能:

  1. 能够进行解引用, 因此要重载operator*
  2. 能够进行指针的->操作来访问存储的成员的空间, 因此要重载operator->
  3. 能够进行迭代器的迭代,比如++等操作。因此要重载++,这里前置和后置++都要重载
  4. 由于list是带头双向循环链表,因此可以进行–操作,要重载–,前置和后置–都要重载
  5. 迭代器要支持是否相等或者不相等,因此要重载!= 和 ==

还有一个要注意的地方,迭代器的应用场景还有一个const迭代器。因此有些情况我们会将容器作为参数传给一个函数,在该函数当中我们并不想去改变容器对象的本身,因此函数的形参是const,我们就需要用const迭代器。具体的一个场景如下:

下面这个函数只想要打印链表的内容,但是不想去改变链表本身,因此形参是const list<int>& l,这个时候const迭代器就派上用场了。

	void print_list(const list<int>& l){// l是const对象,因此cit需要是const的迭代器list<int>::const_iterator cit = l.begin();while (cit != l.end()){cout << *cit << " ";++cit;}cout << endl;}

const迭代器可以通过传三个模版参数去控制调用的是const迭代器还是非const的迭代器

  • list_iterator<T, T&, T*> -> iterator
  • __list_iterator<T, const T&, const T*> -> const_iterator

【要注意不是迭代器这个类型的对象为const对象,不然++等操作会报错】

正向迭代器的代码如下:

	// 迭代器 (通过三个模版参数,来控制调用的是const迭代器还是非const的迭代器)// 【要注意不是迭代器这个类型的对象为const对象】// __list_iterator<T, T&, T*> -> iterator// __list_iterator<T, const T&, const T*> -> const_iteratortemplate<class T, class Ref, class Ptr>struct __list_iterator{typedef __list_node<T> Node;typedef __list_iterator<T, Ref, Ptr> Self;Node* _node; // 迭代器中的_node成员变量,存储着目前迭代器所指向的节点。__list_iterator(Node* node = nullptr):_node(node){}//返回Ref,const就返回const,非const返回非const Ref operator*(){return _node->_data; // this->_node->_data;}// Ptr控制返回的是const属性还是非constPtr operator->(){return &_node->_data;}// 前置++Self& operator++() {_node = _node->_next;return *this;}// 后置++Self& operator++(int) // 加这个int才能让编译器知道这个是后置++的重载{/*__list_iterator<T> tmp = *this;_node = _node->_next;return tmp; */// 考虑用代码复用Self tmp(this->_node); // Self tmp(*this); // 如果不实现拷贝构造,传*this去调用系统默认的拷贝构造也可以实现,但是是浅拷贝,要注意实际是否会出错++(*this);return tmp;}// 前置--Self& operator--(){_node = _node->_prev;return *this;}// 后置--Self& operator--(int){/*__list_iterator<T> tmp = *this;_node = _node->_prev; return tmp; */// 考虑用代码复用Self tmp(this->_node);--(*this);return tmp;}bool operator!=(const Self& it) const{return _node != it._node;}};

list的迭代器实现是list的模拟实现的一个重点,这个内容和之前我们的模拟实现不太一样,是通过封装成一个自定义类型实现的,需要仔细思考。

2.3反向迭代器(重要)

反向迭代器其实也可以像正向迭代器那样去实现,但是也可以借助正向迭代器去实现。

反向迭代器的++就是正向迭代器的–,反向迭代器的–就是正向迭代器的++。我们只需要对正向迭代器的接口进行包装就行了。

简单来说就是:反向迭代器内部可以包含一个正向迭代器

思路:

反向迭代器的rbegin指向最后一个数据,rend指向头节点。

image-20240815165536718

具体实现如下:

	// 反向迭代器 (借助正向迭代器实现)template<class iterator>struct Reverse_list_iterator{// 注意:此处typename的作用是明确告诉编译器,Ref是Iterator类中的一个类型,而不是静态成员变量// 否则编译器编译时就不知道Ref是Iterator中的类型还是静态成员变量// 因为静态成员变量也是按照 类名::静态成员变量名 的方式访问的typedef typename iterator::Ref Ref;typedef typename iterator::Ptr Ptr;typedef Reverse_list_iterator<iterator> Self;iterator _it; // 给一个成员变量是 正向迭代器类型的// 构造函数Reverse_list_iterator(iterator it):_it(it){}// 能够具有指针类似行为的* 和 ->的重载Ref operator*(){iterator temp(_it);--temp; // 对于反向迭代器来说,正向迭代器的迭代器整体往后移动一次,才能正常使用。具体可以画图return *temp;}Ptr operator->(){//return &(operator*()); // 让自己去调用*拿到存储的数据Treturn &(_it._node)->_data; //等价于上面}// 具备移动能力,对++,--等运算符进行重载Self& operator++(){--_it; // 反向迭代器的++就是正向的--return *this;}Self& operator++(int){Self tmp(this->_it); // 也可以Self tmp(*this);--_it;return tmp;}Self& operator--(){++_it;return *this;}Self& operator--(int){Self tmp(this->_it);++_it;return *this;}// 具备相比的能力bool operator!=(const Self& rit) const{return _it != rit._it;}bool operator==(const Self& rit) const{return _it == rit._it;}};

2.3拷贝构造

拷贝构造还是我们老生常谈的问题了,要注意浅拷贝问题的出现。

这里如果有忘记要记得复习之前的内容,这里就直接贴代码了、

	// 拷贝构造list(const list<T>& l){_head = new Node;_head->_next = _head;_head->_prev = _head;// 遍历l,进行深拷贝const_iterator it = l.begin();while (it != l.end()){push_back(*it);++it;}// 要想代码简洁一点。可以用范围for,反正支持迭代期间就能支持范围for}

2.4赋值运算符重载

赋值运算符也要注意浅拷贝问题。

要注意赋值运算符的实现和拷贝构造的区别就是

  • l1(l2)时候的l1是不存在的。

  • l1 = l2的时候l1是存在的,要注意资源的清理,不然会内存泄漏

传统写法:

		// 赋值运算符重载list<T>& operator=(const list<T>& l){// 防止自己给自己赋值if (this != &l){// 先清除掉自身,清除完再尾插。clear(); // 不清除会内存泄漏。// 直接用范围for。for (auto e : l)push_back(e);}return *this;}

现代写法:

	// 赋值运算符——现代写法list<T>& operator=(list<T> l) // l通过拷贝构造就是我们要的{swap(_head, l._head); // 交换一下,把我们不要的给l,结束该作用域l会调用析构函数来销毁,清除我们不要的数据。return *this;}

2.5析构函数

析构函数我们先清除掉除了头节点的所有节点,然后在清除头节点。不能直接清除头节点,不然会造成内存泄漏

	~list(){clear(); // 除了头节点,其他节点都已经释放了。delete _head; // 释放头节点_head = nullptr;}

2.6 clear()

clear要删除除了头节点的所有节点。

clear通过erase的函数复用

		// clear要注意保留头节点(哨兵位),因为clear之后可能继续插入数据。void clear(){iterator it = begin();while (it != end()){erase(it++); // ++的处理后,该迭代器不会失效}}

2.7 erase()

iterator erase(iterator pos)

erase要删除节点,要先将pos位置的节点前后关系处理好,再提前用ret记载好pos位置,

删除pos位置的节点,返回ret。

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;iterator ret = ++pos; // 要返回pos的下一个节点,因为删除该位置的节点后pos会失效。delete cur; // cur是一个指针类型,指向节点(结构体)的指针cur = nullptr;return ret;
}

2.8 push_back()

list是带头双向循环链表,尾插时要处理好关系。如果忘记其基础物理结构要复习。

		void push_back(const T& x){// 无论该链表是否有节点,下面这段代码都能完成任务。Node* tail = _head->_prev;Node* newnode = new Node(x); // 数据已经插入节点了// 接下来要做的就是处理节点之间的链接关系newnode->_next = _head;newnode->_prev = tail; tail->_next = newnode;_head->_prev = newnode;}

实现了insert之后,可以用其代码复用:

		void push_back(const T& x){// 实现insert了之后,可以代码复用insert(end(), x);}

2.9 pop_back()

直接用erase代码复用

要注意由于erase不能删除头节点,因此要传--end()

		void pop_back(){// 用erase代码复用erase(--end());}

2.10 push_front

这里不再具体实现,直接用erase复现。push_back那边有具体实现

		void push_front(const T& x){// 可以和push_back一样,常规实现,也可以用insert代码复用.insert(begin(), x);}

2.11 pop_front

		void pop_front(){// 代码复用erase(begin());}

2.12 insert

		void insert(iterator pos, const T& x){Node* newnode = new Node(x); // 创建要插入的节点Node* cur = pos._node; // cur指向pos位置的节点Node* prev = cur->_prev; // prev指向pos位置的上一个节点cur->_prev = newnode;prev->_next = newnode;newnode->_next = cur;newnode->_prev = prev;}

3.总结:

list模拟实现的总代码和测试代码:

list-04/list.h · WZF-sang/Cpp-Learn - 码云 - 开源中国 (gitee.com)

list的模拟实现的目的并不是将其实现的多好或者多还原,我模拟实现list只是为了去更好的学习其底层原理。本次模拟实现的重点就是迭代器的实现,list的迭代器是一个自定义类型,其通过三个模版参数实现正向迭代器,和借助正向迭代器实现反向迭代器这个过程有助于我更好的理解和学习list这个容器。

并且list和vector在面试的时候经常问到。

image-20240815170932272

第一个问题:

在STL—vector—模拟实现【深度理解vector】【模拟实现vector基本接口】-CSDN博客的最后有讲解,忘了就复习

第二个问题:

vector的底层是一个动态顺序表,是通过三个原生指针实现的,list是带头双向循环链表,底层由一个_prev和一个__next,还有存储的数据data实现。

第三个问题:

vector的增容涉及到开辟新空间,转移数据,删除旧空间,其耗费的代价相较于list来说更大。list不存在增容操作,需要插入就开辟一个节点的空间,然后存数据然后插入。

第四个问题:

迭代器失效其实就是一个迭代器不在精准的指向容器的位置。在list中,只有在删除节点的时候会存在迭代器失效。坐在vector中迭代器失效有两种情况。一种是删除数据,还有一种情况是给了迭代器之后又插入数据

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

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

相关文章

GitHub开源的轻量级文件服务器,可docker一键部署

文件服务器 介绍安装使用命令使用API调用 介绍 项目github官网地址 Dufs是一款由Rust编写的轻量级文件服务器&#xff0c;不仅支持静态文件服务&#xff0c;还能轻松上传、下载、搜索文件&#xff0c;甚至支持WebDAV&#xff0c;让我们通过Web方式远程管理文件变得轻而易举。…

免费生产设备日志采集工具

使用咨询: 扫码添加QQ 永久免费: Gitee下载最新版本 使用说明: CSDN查看使用说明 功能: 定时(全量采集or增量采集) SCADA,MES等系统采集工控机,办公电脑文件. 优势1: 开箱即用. 解压直接运行.插件集成下载. 优势2: 批管理设备. 配置均在后台配置管理. 优势3: 无人值守 采集端…

软考-软件设计师(程序设计语言习题)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 非常期待和您一起在这个小…

Vue: v-html安全性问题

一、问题描述 可能都知道使用v-html插入富文本&#xff0c;存在安全隐患&#xff0c;比如 cross-site scripting attack&#xff08;xss&#xff09;。但具体什么情况下v-html会引发安全问题呢&#xff1f;是否内容中含有<scrpit>标签就会触发执行脚本呢&#xff1f; 二…

基于vue框架的北城招聘管理平台题目7lly3(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,企业,企业信息,职位类型,职位信息,简历信息,职位应聘,求职意愿,面试信息,录取信息,实习信息,冻结信息,解冻信息 开题报告内容 基于Vue框架的北城招聘管理平台 开题报告 一、引言 随着互联网的飞速发展和企业对人才需求的不断增…

Redis的缓存淘汰策略

1. 查看Redis 最大的占用内存 打开redis配置文件, 设置maxmemory参数&#xff0c;maxmemory 是bytes字节类型, 注意转换 2. Redis默认内存多少可以用 注意: 在64bit系统下&#xff0c; maxmemory 设置为 0 表示不限制Redis内存使用 3. 一般生产上如何配置 一般推荐Redis 设置内…

Java中的Map(如果想知道Java中有关Map的知识点,那么只看这一篇就足够了!)

前言&#xff1a;在Java编程语言中&#xff0c;集合框架&#xff08;Collection Framework&#xff09;提供了一系列用于存储和操作数据的接口和类。其中&#xff0c;Map和Set是两个非常重要的接口&#xff0c;分别用于存储键值对和无重复元素的集合。 ✨✨✨这里是秋刀鱼不做梦…

【蓝桥杯集训100题】scratch时间计算 蓝桥杯scratch比赛专项预测编程题 集训模拟练习题第26题

目录 scratch时间计算 一、题目要求 编程实现 二、案例分析 1、角色分析 2、背景分析 3、前期准备 三、解题思路 1、思路分析 2、详细过程 四、程序编写 五、考点分析 六、推荐资料 1、入门基础 2、蓝桥杯比赛 3、考级资料 4、视频课程 5、python资料 scratc…

qtsql连接达梦数据库

odbc window和linux都有odbc的中间件&#xff0c;可以通过odbc中间件配合qtsql连接数据库 windows下配置odbc linux配置odbc apt install unixodbc unixodbc-dev /etc/odbcinst.ini配置 [DM8 ODBC DRIVER] DescriptionDM8 ODBC Driver DRIVER/opt/dmdbms/bin/libdodbc.so/et…

力扣: 两数之和 梦开始的地方

文章目录 需求暴力求解优化一下暴力解法用Map结尾 需求 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案&#xff0c;并且你不能使用…

Leetcode刷题:哈希表

找一个数是否出现过或一个数是否在集合中的时候就要想到用哈希表法 242有效的字母异位词 bool isAnagram(string s, string t) {int table[26];for(char i:s) {table[i-a] 1;}for(char i:t) {table[i-a] -1;}for(int i:table) {if(i!0)return false;}return true;} 349两个数…

docker-harbor私有仓库部署和管理

harbor&#xff1a;开源的企业级的docker仓库软件 仓库&#xff1a;私有仓库 公有仓库 &#xff08;公司内部一般都是私有仓库&#xff09; habor 是有图形化的&#xff0c;页面UI展示的一个工具&#xff0c;操作起来很直观。 harbor每个组件都是由容器构建的&#xff0c;所…

新手教学系列——利用 Loguru 对日志进行分类处理

在现代应用程序中,日志记录是确保系统健康运行的关键因素之一。尤其在复杂的系统中,我们可能需要将日志按不同的需求进行分类和处理。Loguru 作为一款功能强大的日志库,提供了灵活的日志记录方式。今天,我们将探讨如何使用 Loguru 的过滤功能来分类处理系统日志和关键节点日…

算法-矩阵置零(73)

leetcode题目链接 这道题因为要求在O&#xff08;1&#xff09;的空间复杂度下面完成&#xff0c;所以最好的情况就是利用矩阵本身有的元素进行代码编写&#xff0c;而不另外开辟空间。 所以思路如下&#xff1a; 1.遍历第一行第一列&#xff0c;观察是否需要置0&#xff0c…

自定义注解,实现字段加密解密

根据业务需求,要求多部分字段,进行加解密,想到实现方式,就是通过自定义的注解AOP来实现 首先新建一个注解,注意ElementType.FIELD类型,说明这个注解只能作用在字段上 Target({ElementType.FIELD}) Retention(RetentionPolicy.RUNTIME) public interface NeedEncrypt { }在新建…

[CLIP-VIT-L + Qwen] 多模态大模型源码阅读 - trainer篇

[CLIP-VIT-L Qwen] 多模态大模型源码阅读 - trainer篇 前情提要源码阅读导包逐行解读compute_loss方法&#xff08;重构&#xff09;整体含义逐行解读 save_model函数&#xff08;重构&#xff09;整体含义逐行解读 create_optimizer函数&#xff08;重构&#xff09;整体含义…

CI/CD

目录 1.什么是CI/CD? 2.Gitlab仓库部署 3.部署Jenkins 3.1 使用jenkins拉取代码 3.2 对代码进行编译、打包 4.部署tomcat服务器 1.什么是CI/CD? 通俗来说就是启动一个服务&#xff0c;能够监听代码变化&#xff0c;然后自动执行打包&#xff0c;发布等流程: CICD 是持…

Jmeter版本下载国内外镜像源

官网最新版本 https://archive.apache.org/dist/jmeter/binaries/历史版本 https://archive.apache.org/dist/jmeter/binaries/ 国内镜像源1.阿里云 https://mirrors.aliyun.com/apache/jmeter/binaries/2.腾讯云 https://mirrors.cloud.tencent.com/apache/jmeter/

dubbo:dubbo+nacos整合springcloud gateway实现网关(三)

文章目录 0. 引言1. 集成gateway网关1.1 实操步骤1.2 dubbo提供者注册到nacos出现两个实例的问题 2. 源码3. 总结 0. 引言 上次我们讲到使用zookeeper作为注册中心搭建dubbo微服务框架&#xff0c;但是我们还缺少一个服务总入口&#xff0c;也就是我们的网关服务。所以今天我们…

Linux设置内网时间同步

背景&#xff1a;公司有三台服务器检测到同步外网的时间&#xff0c;现需要将其修改为同步公司内网自己搭建的ntp服务器 1、登录服务器检查 同步外网无疑 2、修改配置文件&#xff0c;同步内网ntp服务器时间 配置文件源内容如下&#xff1a; 修改后如下&#xff1a; [rootl…