【C++STL详解(六)】--------list的模拟实现

目录

前言

一、接口总览

一、节点类的模拟实现

二、迭代器类的模拟实现

迭代器的目的

list迭代器为何要写成类?

迭代器类模板参数说明

模拟实现

1.构造函数

2.*运算符重载

3.->运算符重载

4.前置++

5.后置++

6.前置--

7.后置--

8.!=

9.==

三、list类的模拟实现

Ⅰ、默认成员函数

​编辑

1.构造函数

2.拷贝构造

3.赋值重载

4.析构函数

二、迭代器

begin+end

三、访问数据

front和back

四、增删查改

1.insert()

2.erase()

3.push_back

4.pop_back()

5push_front

6.pop_front

7.clear()

8.swap()

五、容量

size

empty()


前言

对于list的模拟实现重点是其迭代器的实现,同时这又是对C++类与对象的一次更深刻的理解与体会!

一、接口总览

namespace li
{//节点结构template<class T>struct ListNode{//成员变量ListNode<T>* _prev;ListNode<T>* _next;T _date;ListNode(const T& value = T());//构造函数};//迭代器类template<class T, class Ref, class Ptr>struct ListIterator{typedef ListNode<T> Node;typedef ListIterator<T, Ref, Ptr> Self;Node* _node;ListIterator(Node* node);Ref operator*();Ptr operator->();//++itSelf& operator++();//it++Self operator++(int);//--itSelf& operator--();//it--Self operator--(int);//it1!=it2bool operator!=(const Self& it);//it1==it2bool operator==(const Self& it);};//list结构template<class T>class list{public:typedef ListNode<T>  Node;typedef ListIterator<T, T&, T*> iterator;typedef ListIterator<T, const T&, const T*> const_iterator;void empty_init()// //iterator///iterator begin();iterator end();const_iterator begin()const;const_iterator end()const;// /// ///构造//list();list(int n, const T& value = T());//迭代器区间初始化template <class Iterator>list(Iterator first, Iterator last);//拷贝构造//it1(it2list(const list<T>& l);//赋值构造//it1=it2list<T>& operator=(list<T> l);//析构函数~list();//modifyiterator insert(iterator pos, const T& x);iterator erase(iterator pos);void push_back(const T& x);void pop_back();void push_front(const T& val);void pop_front();void clear();void swap(list<T>& l);///// List Capacitysize_t size()const;bool empty()const;// List AccessT& front();const T& front()const;T& back();const T& back()const;private:Node* _head;size_t _size;};
}

同样要再自己的命名空间域里面模拟实现!!!

一、节点类的模拟实现

前面我们也有说到list实际上底层就是一个带头双向循环链表,链表的结构就是由一个又一个的结点组成,但是呢,list每次实例化出来的对象数据类型可能不一样,因此我们首先需要实现一个结点类!每一个结点所包含的信息有:数据、前驱指针、后继指针!同时类里面只需要实现一个构造函数。因为该结点类只需根据数据类型去构造去一个结点即可!

//结点类
template<class T>
struct ListNode
{ListNode<T>* _prev;//前驱指针ListNode<T>* _next;//后继指针T _date;//数据ListNode(const T& value = T())//全缺省:_prev(nullptr), _next(nullptr), _date(value){}
};

对于构造函数里面的参数在vector的模拟实现有提及,如果有传值,那就用对应类型的值即可,如果没有,那就使用对应类型的默认构造函数所构造出来的值作为数据!

注意:这个用关键字struct的类,默认的成员变量和成员函数是公开的,类内类外都可以访问!

二、迭代器类的模拟实现

迭代器的目的

在解释原因之前,我们需要了解到一点就是迭代器的意义或者说目的到底是什么?

实际上,迭代器的目的就是:不关注底层的实现细节,能够采用一种类似于指针的方式去访问容器中的内容和数据!说白了就是想要去模拟指针的行为!(++,--,*等操作)

list迭代器为何要写成类?

在前面的string和vector的模拟实现中我们都没有单独的去实现这样一个迭代器类,为啥这里需要呢?实际上就是因为底层空间结构,string和vector是一段连续的空间,他们底层的迭代器就是原生的指针

而list底层的结构是不连续的,随机的,如果采用直接采用原生的指针结点去作为迭代器,那么对于++这类的操作符,一个结点能进行吗?很显然是不能的,因为不是连续的空间!

所以,对于list的迭代器,虽说本质还是原生的结点指针,但是不能直接采用,因为原生的结点指针不能够满足我们所需要的迭代器的行为,也就是它不能像vector和string的迭代器那样直接进行++,--,*等操作!

所以的所以,内置类型不能满足我们所需要的行为时,我们可以将其封装成自定义类型,也就是将原生的结点指针封装成一个类,这样就变成了自定义类型,对于一个类,我们就可以进行运算符的重载!比如表面上是对迭代器进行++操作,其底层实际上是node=node->next!这不是就符合迭代器存在的目的吗?

总结:list迭代器,由于原生结点不能满足我们所需的行为,因此要将其封装成一个类,也就是变成自定义类型,就能控制它的行为!

迭代器类模板参数说明

template<class T, class Ref, class Ptr>

这个参数的存在就是就是因为迭代器实际有两种,一个是非const,一个为const对象提供的。而这两种迭代器的底层实现起来其实大差不差,为了让代码不冗余,所以去定义了这个重定义类型。可以看list类中的这几个重定义类型

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

可以看到Ref对应的是T引用,Ptr对应的就是T指针,他们会根据传进来的类型自动匹配!如果不设计就很难区分。

为啥要一个引用,一个指针呢?实际和运算符重载有关,接着向下看!

模拟实现

1.构造函数

本质还是结点指针,所以迭代器需要传进来一个结点指针完成构造!

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

2.*运算符重载


//*it
Ref operator*()
{return _node->_date;
}

这个操作实际上相当于指针的解引用,*it,访问数据,对于解引用操作,我们不仅可以对当前是数据进行读操作,还能重新赋值,也就是可读可写,所以采用引用返回!所以原本是T&,但由于要区分const T&,就用了Ref这个模板参数,这就是它的由来

3.->运算符重载

这个的存在,在某些场景下面用迭代器是可能会用到->操作!

如下场景:

如我们的容器里面的数据类型不是内置类型,而是自定义类型时,可能会使用到这个运算符

比如上面这段代码,在*it时会发生错误,因为解引用只是访问到A而已,没有访问到里面的成员变量

改法1:

改法2:

这里就需要使用到运算符->重载,实际上这里完整的写法是:it.operator->()->_a,缩写就是两个->->,第一个->实际上获取的是A*,第二个是对A*指针的解引用!编译器为了代码可读性,省略了一个!

有上面的知识可以得出重载的底层实现了,实际上就是返回当前数据的地址

//为了像指针那样去操作:it->;
Ptr operator->()
{return &_node->_date;
}

这里原本的返回类型是T*的,但是由于要区分const T*, 所以设计一个模板参数叫做Ptr,它会根据传进来的数据类型进行实例化,这也是它由来的原因!

注意:-->操作要求成员变量是公有的才可以访问!

4.前置++

前置++要求是当前对象先自增完才返回自增后的数据,自增的操作实际上就是让指针向后移动

//++it
Self& operator++()
{_node = _node->_next;return *this;
}

注意:Self为当前迭代器的类型

typedef ListIterator<T, Ref, Ptr> Self;

5.后置++

后置++,要先返回加之前的结果,然后再++,所以可以拷贝构造出一个临时变量,然后再自增,最后再返回这个临时变量即可!注意了,这里不能用引用返回,而是使用传值返回,因为临时变量具有常性的原因,可读不可写

//it++
Self operator++(int)
{Self tmp(*this);//虽然这个类,没有拷贝构造,但是默认生成的拷贝构造就已经够用了,因为迭代器只需要浅拷贝就好了_node = _node->_next;return tmp;
}

需要注意后置的写法!!!这个再类和对象二有提及

6.前置--

这个和前置++一样的思路,让指针前移即可!

Self& operator--()
{_node = _node->_prev;return *this;
}

7.后置--

Self operator--(int)
{Self tmp(*this);_node = _node->_prev;return tmp;
}

8.!=

注意这里是比较两个结点的指针,不是里面的数据,如果比较数据,那就扯淡了,因为可能数据会相同!

bool operator!=(const Self& it)
{return _node != it._node;
}

9.==

也是比较指针!

bool operator==(const Self& it)
{return _node == it._node;
}

其实可以看出上一章list的介绍与使用中,为什么,list的迭代器只能++,而不能是begin()+5等等之类的了,因为底层压根没有重载,同时它本身的结构也不支持!

容器之间的迭代器底层实现是不一样的,这取决于他们的底层结构,但是表面上使用起来类似,都是去模仿指针的行为!

三、list类的模拟实现

Ⅰ、默认成员函数

成员变量无需多言,就是一个结点指针,外加一个size!以及额外的三个重命名类型!

public:
typedef ListNode<T>  Node;
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;//成员变量
private:
Node* _head;
size_t _size;

除此之外,我们新设置了个成员函数初始化空链表的,因为每次构造都需要先构造一个空的链表,然后再进行后续操作!

void empty_init()
{_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;
}

1.构造函数

  • 无参数构造
list()
{empty_init();
}
  • 特定值初始化
list(int n, const T& value = T())
{empty_init();for (int i = 0; i < n; i++){push_back(value);//尾插数据即可}
}
  • 迭代器区间初始化
template <class Iterator>
list(Iterator first, Iterator last)
{empty_init();while (first != last){push_back(*first);++first;}}

这里和vector的类似!

2.拷贝构造

先初始化一个头,再尾插即可!

//拷贝构造
//it1(it2)
list(const list<T>& l)
{empty_init();for (auto& e : l){push_back(e);}
}

3.赋值重载

这里我们采用现代写法!使用复用拷贝构造+swap交换

//赋值构造
//it1=it2
list<T>& operator=(list<T> l)//引用返回支持连续赋值
{swap(l);return *this;
}

这里也可以采用类似 string类中传统写法,先清理容器,再一个个尾插!

//赋值构造
//it1=it2
list<T>& operator=(const list<T>& l)
{if (this != &l)//避免自己给自己赋值{clear();//清理容器for (auro& e : l){push_back(e);//尾插到it1}}return *this;}

4.析构函数

先清理内容,再释放头结点!

//析构函数
~list()
{clear();delete _head;_head = nullptr;
}

二、迭代器

begin+end

需知:begin():返回的是第一个有效数据的迭代器

end():返回的是最后一个有效数据的下一个位置的迭代器

iterator begin()
{return iterator(_head->_next);//会去调用迭代器类构造一个迭代器,再返回//return _head->_next;也可以这样去写,因为单参数的构造函数支持隐式类型的转化//而iterator的构造函数实际上就是单参数的构造
}iterator end()
{return iterator(_head);
}//const迭代器
const_iterator begin()const
{return iterator(_head->_next);
}const_iterator end()const
{return iterator(_head);
}

对于const迭代器,不能写成const iterator,这样性质就不同了,因为这个表示的是迭代器本身不能修改,而我们期望的是指向的内容不能修改,所以const iterator不是我们需要的迭代器!

一定要注意区分这里的iterator和vector中的iterator,这里的iterator已经被重命名了,它实际上是一个类!

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

三、访问数据

front和back

front:返回list的第一个结点中值的引用,就是取头数据

back:返回list的最后一个结点中值的引用,就是取尾数据

T& front()
{return _head->_next->_date;
}
const T& front()const
{return _head->_next->_date;
}
T& back()
{return _head->_prev->_date;
}
const T& back()const
{return _head->_prev->_date;
}

四、增删查改

1.insert()

在pos位置前插入操作,并返回pos位置的迭代器!这个pos实际上也是个迭代器!具体可以看上节的原函数list的介绍与使用!这个只模拟实现一个就是插入值的操作!

这里的插入,实际上就是new一个结点出来,在进行插入操作即可!

iterator insert(iterator pos, const T& x)
{Node* newnode = new Node(x);Node* cur = pos._node;Node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return pos;
}

2.erase()

删除pos位置的值,并返回pos位置的迭代器。所以这里会存在迭代器失效的问题,注意只是当前迭代器失效,之后的其他迭代器并不会受到任何影响!因为每一个迭代器都是由一个个结点构造出来的!

iterator erase(iterator pos)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;--_size;return next;
}

需要注意删除操作,我们需要先保存当前结点的前一个结点和后一个结点,这样才能将其前后的两个结点连接起来!

3.push_back

尾插操作,实际上就是在end()位置插入,就是头结点的前面插入!直接复用insert

void push_back(const T& x)
{insert(end(), x);
}

4.pop_back()

尾删操作,直接复用erase即可!

void pop_back()
{erase(--end());//这里只能--end,不能end-1因为end是传值返回的
}

5push_front

头插操作,直接复用!

void push_front(const T& val)
{insert(begin(), val);
}

6.pop_front

头删操作,直接复用!

void pop_front()
{erase(begin());
}

7.clear()

清理工作,保留头节点!

void clear()
{iterator it = begin();while (it != end()){it=erase(it);//注意这里一定要更新迭代器,因为会失效}
}

8.swap()

void swap(list<T>& l)
{std::swap(_head, l._head);std::swap(_size, l._size);
}

直接复用全局的swap

五、容量

size

这里我们在插入和删除操作,都加上了++size或者--size了。所以直接调用即可!

size_t size()const
{return _size;
}

empty()

bool empty()const
{return _size == 0;
}

好了今天的分享就到这里,我认为这是我学C++的第二个难点,重点就是迭代器类的模拟实现,它再一次体现了类和对象的特点以及封装的特性。值得反复去体会和反思!!!

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

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

相关文章

【Mac】graphpad prism for Mac(专业医学绘图工具) v10.2.3安装教程

软件介绍 GraphPad Prism for Mac是一款专业的科学数据分析和绘图软件&#xff0c;广泛用于生物医学和科学研究领域。它具有强大的统计分析功能&#xff0c;可以进行各种数据分析&#xff0c;包括描述性统计、生存分析、回归分析、方差分析等。同时&#xff0c;它还提供了丰富…

滑动验证码登陆测试编程示例

一、背景及原理 处理登录时的滑动验证码有两个难点&#xff0c;第一个是找到滑块需要移动的距离&#xff0c;第二个是模拟人手工拖动的轨迹。模拟轨迹在要求不是很严的情况下可以用先加速再减速拖动的方法&#xff0c;即路程的前半段加速度为正值&#xff0c;后半段为负值去模…

Go未用代码消除与可执行文件瘦身

在日常编写Go代码时&#xff0c;我们会编写很多包&#xff0c;也会在编写的包中引入了各种依赖包。在大型Go工程中&#xff0c;这些直接依赖和间接依赖的包数目可能会有几十个甚至上百个。依赖包有大有小&#xff0c;但通常我们不会使用到依赖包中的所有导出函数或类型方法。 这…

如何高速下载,百度 阿里 天翼 等网盘内的内容

如何高速下载&#xff0c;百度 阿里 天翼 等网盘内的内容&#x1f3c5; 前言教程下期更新预报&#x1f3c5; 前言 近段时间经常给大家分享各种视频教程&#xff0c;由于分享的资料是用迅雷网盘存的&#xff0c;但是绝大部分用户都是使用的某度&#xff0c;阿某的这些网盘&…

VScode添加c/c++头文件路径

1.设置工作区include path方法&#xff1a; 命令面板 -> 输入c/c 修改配置文件&#xff0c;添加路径&#xff1a; 2.全局路径&#xff1a; 设置 - > 搜索include path

tomcat+maven+java+mysql图书管理系统1-配置项目环境

目录 一、软件版本 二、具体步骤 一、软件版本 idea2022.2.1 maven是idea自带不用另外下载 tomcat8.5.99 Javajdk17 二、具体步骤 1.新建项目 稍等一会&#xff0c;创建成功如下图所示&#xff0c;主要看左方目录相同不。 给maven配置国外镜像 在左上…

【DPU系列之】Bluefield 2 DPU卡的功能图,ConnectX网卡、ARM OS、Host OS的关系?(通过PCIe Switch连接)

核心要点&#xff1a; CX系列网卡与ARM中间有一个PCIe Swtich的硬件单元链接。 简要记录。 可以看到图中两个灰色框&#xff0c;上端是Host主机&#xff0c;下端是BlueField DPU卡。图中是BF2的图&#xff0c;是BF2用的是DDR4。DPU上的Connect系列网卡以及ARM系统之间有一个…

cmd命令跳转至指定目录

1、指定目录与当前目录在同一盘符&#xff1a;直接cd 指定目录。2、指定目录与当前目录不在同一盘符&#xff1a; a、方法一&#xff1a;cd 指定目录&#xff0c;此时不会跳转&#xff0c;接着再输入指定目录的盘符即可。 b、方法二&#xff1a;输入指定目录所在的盘符&#xf…

C++:map和set类

关联式容器 在初阶阶段&#xff0c;我们已经接触过STL中的部分容器&#xff0c;比如&#xff1a;vector、list、deque、 forward_list(C11)等&#xff0c;这些容器统称为序列式容器&#xff0c;因为其底层为线性序列的数据结构&#xff0c;里面 存储的是元素本身。那什么是关…

Docker的私有仓库部署-Harbor

目录 一. Docker原生私有仓库 Registry 1. Registry 的介绍 2. Registry 的部署过程 二. Registry 的升级——Habor 1. Harbor 简介 2. Harbor 特性 3. Harbor 的构成 4. Harbor 部署 4.1 部署 Docker-Compose 服务 4.2 部署 Harbor 服务 4.2.1 下载或上传 Harbor…

结构体介绍(2)

结构体介绍&#xff08;2&#xff09; 前言一、结构体的内存对齐之深入理解为什么存在内存对齐&#xff1f;修改默认对齐数 二、结构体传参2.1&#xff1a;该怎么传参呢&#xff1f; 三、结构体实现位段3.1什么是位段位段的内存分配位段的跨平台问题 总结 前言 根据之前讲了结…

【C++程序员的自我修炼】string 库中常见用法(二)

制芰荷以为衣兮 集芙蓉以为裳 不吾知其亦已兮 苟余情其信芳 目录 字符串的头部插入insert <1>头部插入一个字符串&#xff1a; <2>头部插入一个字符&#xff1a; <3>迭代器的插入 总结&#xff1a; 字符串的头部删除 erase <1>头部插入删除一个字符&a…

nodejs实战——搭建websocket服务器

本博客主要介绍websocket服务器库安装&#xff0c;并举了一个简单服务器例子。 服务器端使用websocket需要安装nodejs websocket。 cd 工程目录 # 此刻我们需要执行命令&#xff1a; sudo npm init上述命令创建package.json文件&#xff0c;系统会提示相关配置。 我们也可以使…

数据结构十:哈希表

本次将从概念上理解什么是哈希表&#xff0c;理论知识较多&#xff0c;满满干货&#xff0c;这也是面试笔试的一个重点区域。 目录 一、什么是哈希表 1.0 为什么会有哈希表&#xff1f; 1.1 哈希表的基本概念 1.2 基本思想 1.3 举例理解 1.4 存在的问题 1.5 总结 二、…

基于JSP的人才公寓管理系统

目录 背景 技术简介 系统简介 界面预览 背景 随着互联网的广泛推广和应用&#xff0c;人才公寓管理系统在网络技术的推动下迅速进步。该系统的设计初衷是满足住户的实际需求&#xff0c;通过深入了解住户的期望&#xff0c;开发出高度定制化的人才公寓管理系统。利用互联网…

Django关于ORM的增删改查

Django中使用orm进行数据库的管理&#xff0c;主要包括以下步骤 1、创建model&#xff0c; 2、进行迁移 3、在视图函数中使用 以下的内容可以先从查询开始看&#xff0c;这样更容易理解后面删除部分代码 主要包括几下几种&#xff1a; 1、增 1&#xff09;实例例化model,代…

SpringTask定时任务

SpringBoot项目定时任务 首先在启动类引入注解EnableScheduling然后在方法中加注解Scheduled(cron“”)cron表达式 生成cron https://www.pppet.net/

流畅的Python阅读笔记

五一快乐的时光总是飞快了&#xff0c;不知多久没有拿起键盘写文章了&#xff0c;最近公司有Python的需求&#xff0c;想着复习下Python吧&#xff0c;然后就买了本Python的书籍 书名&#xff1a; 《流畅的Python》 下面是整理的一个阅读笔记&#xff0c;大家自行查阅&#xf…

【Qt QML】Frame组件

Frame&#xff08;框架&#xff09;包含在&#xff1a; import QtQuick.Controls继承自Pane控件。用于在可视框架内布局一组逻辑控件。简单来说就是用来包裹和突出显示其他可视元素。Frame不提供自己的布局&#xff0c;但需要自己对元素位置进行设置和定位&#xff0c;例如通过…

Numerical Analysis(byRichard.L..Burden)【pdf高清英文原版】

专栏导读 作者简介&#xff1a;工学博士&#xff0c;高级工程师&#xff0c;专注于工业软件算法研究本文已收录于专栏&#xff1a;《有限元编程从入门到精通》本专栏旨在提供 1.以案例的形式讲解各类有限元问题的程序实现&#xff0c;并提供所有案例完整源码&#xff1b;2.单元…