【C++】List模拟实现过程中值得注意的点

👀樊梓慕:个人主页

 🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》《算法》

🌝每一个不曾起舞的日子,都是对生命的辜负


目录

前言

1.List迭代器

2.适配器

3.迭代器失效

4.模拟实现源码


前言

本篇文章旨在记录博主在模拟实现vector容器中遇到的一些问题,都是一些需要注意的细节问题,希望与大家共勉。


欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。

=========================================================================

GITEE相关代码:🌟fanfei_c的仓库🌟

=========================================================================


1.List迭代器

在之前模拟实现『 String和Vector』时,我们都采用的『 原生指针』实现迭代器,那么到List这里还可以么?

这我们就要搞清楚『 迭代器存在的意义』。

我们希望可以通过迭代器来实现对某一容器的遍历访问,例如对迭代器++或--;

迭代器:让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问。

之前的String和Vector我们可以简单的利用原生指针实现迭代器是因为String和Vector底层的物理空间是连续的,所以++和--当然可以访问到对应的对象。

可到了List中,List的底层是带头双向循环链表,链表的物理空间并『 不连续』,所以我们需要对迭代器进行一定的『 封装』,让迭代器『 符合统一的迭代器使用规则』。

如何封装呢?

提示:比如重载各种运算符,++运算符的底层可以为链表的p=p->next;

注意:end迭代器是最后一个元素的下一个位置,所以end一般指向的是『 头节点』。 

  • 头节点不存储数据。

 

// List的节点类
template<class T>
struct ListNode
{ListNode<T>* _next;ListNode<T>* _prev;T _data;ListNode(const T& val = T()):_next(nullptr), _prev(nullptr), _data(val){}
};
//List的迭代器类
template<class T, class Ref, class Ptr>
class __list_iterator
{
public:typedef ListNode<T> Node;typedef __list_iterator<T, Ref, Ptr> Self;Node* _node;__list_iterator(Node* x):_node(x){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}Self& operator++(){_node = _node->_next;return *this;}Self operator++(int){Self 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;}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}
};

2.适配器

我们知道迭代器一般需要两个形式,一种为非const迭代器,一种为const迭代器用来满足不同的使用场景,但你有没有发现上面的代码,似乎只有一种形式???

其实并不是,如果我们按照以前的写法,应为:

//非const迭代器类
template<class T>
class __list_iterator
{
public:typedef ListNode<T> Node;typedef __list_iterator<T> Self;Node* _node;__list_iterator(Node* x):_node(x){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}Self& operator++(){_node = _node->_next;return *this;}Self operator++(int){Self 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;}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}
};
//const迭代器类
template<class T>
class __list_const_iterator
{
public:typedef ListNode<T> Node;typedef __list_const_iterator<T> self;Node* _node;__list_const_iterator(Node* x):_node(x){}const T& operator*(){return _node->_data;}const T* operator->(){return &_node->_data;}self& operator++(){_node = _node->_next;return *this;}self operator++(int){self 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;}bool operator!=(const self& s){return _node != s._node;}bool operator==(const self& s){return _node == s._node;}
};

但这样看起来代码非常冗余,那么有没有什么方式来简化代码呢?

其实模板的用法不仅有vector<int>这类,还可以这么使用:


所以,编译器可以通过模板自动推演出当前使用场景为const迭代器还是非const迭代器。

这种方式可以唤作为『 适配器』,当然这部分后面还会有涉及。


3.迭代器失效

在之前学习vector时我们发现了迭代器失效的问题,对于vector来讲迭代器失效往往发生在扩容之后,那么对于List来讲,是不需要扩容的,那么List会不会发生迭代器失效的问题呢?

List可能会在『 erase』操作之后,迭代器失效。

我们来分析一下:

vector迭代器失效的原因是因为扩容导致迭代器使用前后底层指针位置发生了改变,换句话说就是开辟了新的空间,指针指向了新的地址,而迭代器没有更新从而导致迭代器失效。

而List并不会扩容,也不会开辟新的连续的空间,所以对于插入来说无从谈起迭代器失效,而erase是会导致迭代器失效的,因为删除了该位置的对象,迭代器底层指针成为野指针,但并不会影响其他迭代器,只需要重新赋值即可,比如设计为删除当前迭代器位置对象,并指向下一位置:

// 删除pos位置的节点,返回该节点的下一个位置
iterator erase(iterator pos)
{assert(pos != end());//list是带头双向循环链表,当pos是end()位置时,证明没有可删除的节点了Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;return next;
}

4.->操作符的重写

->操作符的重写是一个特例,大家只需要记住即可,因为这是STL设计者故意设计为这样的。

Ptr operator->()//为什么返回的是地址??
{return &_node->_data;
}

那放在实际的场景中我们使用一下看看: 

struct AA
{int _a1;int _a2;AA(int a1 = 1, int a2 = 1):_a1(a1), _a2(a2){}
};void test_list5()
{list<AA> lt;AA aa1;lt.push_back(aa1);lt.push_back(AA());AA aa2(2, 2);lt.push_back(aa2);lt.push_back(AA(2, 2));list<AA>::iterator it = lt.begin();while (it != lt.end()){cout << it->_a1 << ":" << it->_a2 << endl;//这里打印的是_a2的内容并不是_a2的地址++it;}cout << endl;
}

正常来讲这里本来是应该有两个->的,第一个箭头是it->去调用重载的operator->返回AA* 的指针,第二个箭头是AA* 的指针去访问对象当中的成员变量_a2。

cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << endl;//实际

但是为了程序可读性,所以编译器做了特殊识别处理,省略了一个箭头。 


5.模拟实现源码

#pragma once#include<iostream>
#include<assert.h>namespace F
{// List的节点类template<class T>struct ListNode{ListNode<T>* _next;ListNode<T>* _prev;T _data;ListNode(const T& val = T()):_next(nullptr),_prev(nullptr),_data(val){}};//List的迭代器类template<class T, class Ref, class Ptr>class __list_iterator{public:typedef ListNode<T> Node;typedef __list_iterator<T, Ref, Ptr> Self;Node* _node;__list_iterator(Node* x):_node(x){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}Self& operator++(){_node = _node->_next;return *this;}Self operator++(int){Self 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;}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}};//list类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;public:///// List的构造void empty_init(){_head = new Node;_head->_next = _head;_head->_prev = _head;}list(){empty_init();}list(int n, const T& value = T()){empty_init();while (n--){push_back(value);}}list(const list<T>& l){empty_init();for (const auto& e : l){push_back(e);}}list<T>& operator=(list<T> l){swap(l);return *this;}~list(){clear();delete _head;_head = nullptr;}///// List Iteratoriterator begin(){return _head->_next;}iterator end(){return _head;}const_iterator begin() const{return _head->_next;}const_iterator end() const{return _head;}///// List Capacitysize_t size()const{size_t count = 0;const_iterator it = begin();while (it != end()){++count;++it;}return count;}bool empty()const{return _head->_next == _head;}// List AccessT& front(){return *begin();}const T& front()const{return *begin();}T& back(){return *(--end());}const T& back()const{return *(--end());}// List Modifyvoid push_back(const T& val) { insert(end(), val); }    void pop_back() { erase(--end()); }void push_front(const T& val) { insert(begin(), val); }void pop_front() { erase(begin()); }// 在pos位置前插入值为val的节点iterator insert(iterator pos, const T& val){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(val);prev->_next = newnode;newnode->_next = cur;newnode->_prev = prev;cur->_prev = newnode;//return iterator(newnode);return newnode;//单参数的构造函数支持隐式类型转换}// 删除pos位置的节点,返回该节点的下一个位置iterator erase(iterator pos){assert(pos != end());//list是带头双向循环链表,当pos是end()位置时,证明没有可删除的节点了Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;return next;}void clear(){iterator it = begin();while (it != end()){it = erase(it);}}void swap(list<T>& l){std::swap(_head, l._head);}private:Node* _head;};
};

模拟实现的主要目的在于学习优秀的代码,比如这里适配器的使用、优秀的框架设计。

注意为了迭代器统一使用:

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

这样我们就可以不需要知道迭代器的底层是如何实现的,只需要调用迭代器的接口就可以了,从而实现了通用的目的。


=========================================================================

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎

🌟~ 点赞收藏+关注 ~🌟

=========================================================================

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

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

相关文章

格密码基础:详解LWE问题(2)

目录 一. LWE问题的标准式 二. LWE单向函数与SIS单向函数 2.1 SIS问题的标准型 2.2 SIS与LWE标准型之间的关系 三. LWE问题有多难&#xff1f; 3.1 结论 3.2 归约过程 四. LWE归约性质 五. LWE问题的两个版本 一. LWE问题的标准式 系列文章&#xff1a; 格密码基础&…

Java SE入门及基础(25)

目录 方法带参&#xff08;续第24篇&#xff09; 6.方法参数传递规则 方法传参来自官方的说明 基本数据类型传值案例 基本数据类型传值时传递的是值的拷贝 引用数据类型传值案例 引用数据类型传值时传递的是对象在堆内存上的空间地址 Java SE文章参考:Java SE入门及基础知…

文件操作和IO(1)

认识文件 先来认识狭义上的文件(存储在硬盘(磁盘)上).针对硬盘这种持久化的I/O设备,当我们想要进行数据保存时,往往不是保存成一个整体,而是独立成一个个的单位进行保存,这个独立的单位就被抽象成文件的概念,就类似办公桌上的一份份真实的文件一般. 注意:硬盘 ! 磁盘 磁盘属于…

基于C++11的数据库连接池【C++/数据库/多线程/MySQL】

一、概述 概述&#xff1a;数据库连接池可提前把多个数据库连接建立起来&#xff0c;然后把它放到一个池子里边&#xff0c;就是放到一个容器里边进行维护。这样的话就能够避免数据库连接的频繁的创建和销毁&#xff0c;从而提高程序的效率。线程池其实也是同样的思路&#xf…

Java-NIO篇章(4)——Selector选择器详解

Selector介绍 选择器&#xff08;Selector&#xff09;是什么呢&#xff1f;选择器和通道的关系又是什么&#xff1f;这里详细说明&#xff0c;假设不用选择器&#xff0c;那么一个客户端请求数据传输那就需要建立一个连接&#xff0c;为了避免线程阻塞&#xff0c;那么每个客…

【Linux】进程间通信——system V 共享内存、消息队列、信号量

需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网&#xff0c;轻量型云服务器低至112元/年&#xff0c;优惠多多。&#xff08;联系我有折扣哦&#xff09; 文章目录 写在前面1. 共享内存1.1 共享内存的概念1.2 共享内存的原理1.3 共享内存的使用1.3.1 …

磁盘分区机制

lsblk查看分区 Linux分区 挂载的经典案例 1. 虚拟机增加磁盘 点击这里&#xff0c;看我的这篇文章操作 添加之后&#xff0c;需要重启系统&#xff0c;不重启在系统里看不到新硬盘哦 出来了&#xff0c;但还没有分区 2. 分区 还没有格式化 3. 格式化磁盘 4. 挂载 5. 卸载…

国标GB28181安防视频监控EasyCVR级联后上级平台视频加载慢的原因排查

国标GB28181协议安防视频监控系统EasyCVR视频综合管理平台&#xff0c;采用了开放式的网络结构&#xff0c;可以提供实时远程视频监控、视频录像、录像回放与存储、告警、语音对讲、云台控制、平台级联、磁盘阵列存储、视频集中存储、云存储等丰富的视频能力&#xff0c;同时还…

一、用户管理中心——前端初始化

一、Ant Design Pro初始化 1.创建空文件夹 2.打开Ant Design Pro官网 3.打开终端进行初始化 在终端输入npm i ant-design/pro-cli -g 在终端输入pro create myapp 选择umi3 选择simple 项目创建成功后&#xff0c;在文件夹中出现myapp 4.安装依赖 使用vscode打开项目 …

STL中的stack、queue以及deque

目录 一、关于deque容器&#xff08;双端队列&#xff09; 1、deque的底层实现 2、deque的缺点 3、关于stack与squeue默认使用deque容器 二、stack简介 1、stack的成员函数&#xff08;接口&#xff09; 2、stack的模拟实现 三、queue简介 1、queue的成员函数&#xff08…

安全防御-基础认知

目录 安全风险能见度不足&#xff1a; 常见的网络安全术语 &#xff1a; 常见安全风险 网络的基本攻击模式&#xff1a; 病毒分类&#xff1a; 病毒的特征&#xff1a; 常见病毒&#xff1a; 信息安全的五要素&#xff1a; 信息安全的五要素案例 网络空间&#xff1a…

docker配置阿里云镜像加速器

1、阿里云镜像加速器地址获取&#xff1a; 2、配置ECS镜像加速器&#xff0c;重启docker mkdir -p /etc/docker tee /etc/docker/daemon.json <<-EOF {"registry-mirrors": ["https://2lg9kp55.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-…

谈判(贪心算法)

题目 import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Scanner;public class Main {public static void main(String[] args) { Scanner sc new Scanner(System.in);int n sc.nextInt();sc.nextLine();List<Integ…

H3C交换机S6850配置M-LAG三层转发

正文共&#xff1a;1999 字 30 图&#xff0c;预估阅读时间&#xff1a;3 分钟 前面提到M-LAG是一种跨设备链路聚合技术&#xff0c;将两台物理设备在聚合层面虚拟成一台设备来实现跨设备链路聚合&#xff0c;从而提供设备级冗余保护和流量负载分担。 之前已经做了DRNI的三层转…

微前端小记

步骤 将普通的项目改造成 qiankun 主应用基座&#xff0c;需要进行三步操作&#xff1a; 1. 创建微应用容器 - 用于承载微应用&#xff0c;渲染显示微应用&#xff1b; a. 设置路由routeb.主应用的布局包括&#xff1a; 主应用菜单&#xff0c;用于渲染菜单主应用渲染区域&a…

ubuntu安装vm和Linux

1、下载Ubuntu Index of /releaseshttps://old-releases.ubuntu.com/releases/ 2、下载VMware 官方正版VMware下载&#xff08;16 pro&#xff09;&#xff1a;https://www.aliyundrive.com/s/wF66w8kW9ac 下载Linux系统镜像&#xff08;阿里云盘不限速&#xff09;&#xff…

webpack 核心武器:loader 和 plugin 的使用指南(上)

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

Twisted Circuit洛谷绿题题解

Twisted Circuit 题面翻译 读入四个整数 0 0 0 或者 1 1 1&#xff0c;作为如图所示的电路图的输入。请输出按照电路图运算后的结果。 感谢PC_DOS 提供的翻译 题目描述 输入格式 The input consists of four lines, each line containing a single digit 0 or 1. 输出格…

读书笔记之《万物起源》:宇宙与人类的极简史

《万物起源&#xff1a;从宇宙大爆炸到文明的兴起》讲述了从大爆炸直到今日&#xff0c;约140亿年间所有重大事物的起源&#xff0c;依次覆盖了量子力学&#xff0c;天体物理学&#xff0c;化学&#xff0c;行星科学&#xff0c;地质学&#xff0c;生物学和人类历史等等学科。 …

08- OpenCV:形态学操作(膨胀与腐蚀 、提取水平与垂直线)

目录 前言 一、膨胀&#xff08;Dilation&#xff09;与 腐蚀&#xff08;Erosion&#xff09; 二、形态学操作 1、开操作&#xff08;Opening&#xff09; 2、闭操作&#xff08;Closing&#xff09; 3、形态学梯度&#xff08;Morphological Gradient&#xff09; 4、…