【C++初阶】STL详解(八)List的模拟实现

本专栏内容为:C++学习专栏,分为初阶和进阶两部分。 通过本专栏的深入学习,你可以了解并掌握C++。

💓博主csdn个人主页:小小unicorn
⏩专栏分类:C++
🚚代码仓库:小小unicorn的代码仓库🚚
🌹🌹🌹关注我带你学习编程知识

STL详解(八)

  • list的再认识:
  • 初始化与定义节点:
  • 迭代器实现:
    • 构造:
    • ++
    • 解引用:*
    • !=
    • 基本框架搭建:
    • --
    • 后置++与后置--
    • ->
    • ==
    • const迭代器
      • 拓展:
      • 拓展2:
  • 相关函数接口:
    • Insert:
    • erase:
    • push_front与pop_fronr
    • push_back与pop_back
    • size:
    • clear与析构:
    • 拷贝构造:
    • 赋值重载:
      • 传统写法:
      • 现代写法:
  • 对比vector与list

list的再认识:

在之前List的介绍与使用中,我们知道list容器是一个带头双向循环链表,那么我们在模拟实现中,能不能先证明一下List是否是一个双向循环链表呢?

我们参考一下stl中list的源码:
在这里插入图片描述
我们看到,在源码中,list中有一个__list_node的节点,我们将这个链表的节点定义打开发现定义两个指针next,prev.

再来看一下它的空初始化:
在这里插入图片描述
通过观察源码中list的初始化,确实是一个双向循环链表。

接下来。我们就来自己实现一下里面的接口函数。
注意:在模拟实现中,我们采取用与与源码中相同的命名风格。
为防止与库里面的list重复,我们模拟实现将定义在自己的命名空间中。

初始化与定义节点:

首先,我们需要定义三个类,并用摸版进行封装:分别是list,list的节点,以及迭代器:

list节点:

template<class T>
struct list_node
{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& x=T()):_data(x),_next(nullptr),_prev(nullptr)
{}
};

list:

template<class T>
class list
{typedef list_node<T> Node;
public://空初始化:void empty_init(){_head = new Node;_head->_next = _head;_head->_prev = _head;}//无参构造:list(){empty_init();}void push_Back(const T& x){Node* tail = _head->_prev;Node* newnode = new Node(x);tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;}
private:Node* _head;
};

这里我们写的是无参构造,以及实现了一个尾插接口:
尾插双向链表实现已经再简单不过了:
在这里插入图片描述
现在我们测试一下:
在这里插入图片描述
现在还不能进行遍历,因此我们自己要实现一个迭代器:

迭代器实现:

那么这个迭代器我们要作为怎么实现呢?
我们可以先回顾一下,在vector中,我们实现迭代器就是实现原生指针。
在这里插入图片描述
在vector中,给it解引用就可以访问到里面的数据,但是链表不行,因为链表中空间不是连续的。
那么应该怎么实现呢?其实这个就和我们之前的日期类一样,在日期类中我们用运算符重载与封装实现了对日期类的++操作。而我们的迭代器也使这样实现的。

这里我们需要实现迭代器的!=,*与++操作:
在这里插入图片描述
我们先看一下库里面的操作:
在这里插入图片描述

构造:

看一下库里面的操作:
在这里插入图片描述
库里面用了一个节点的指针进行构造,这是因为:单参数的构造函数支持隐式类型转换。

所以我们的构造就可以这样写:

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

++

实现迭代器++,就是指针往后走的过程,注意返回的是迭代器。我们可以将迭代器重命名一下:

typedef __list_iterator<T> self;

实现代码:

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

解引用:*

返回节点里面的数据即可:

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

!=

两个迭代器进行比较,实质两个指针比较。

//两个迭代器进行比较,两个指针比较
bool operator!=(const self& s)
{return _node  !=  s._node;
}

基本框架搭建:

这样基本上迭代器的基本架子就完成了:

typedef __list_iterator<T> iterator;
iterator begin()
{return _head->_next;
}
iterator end()
{return _head;
}

在list中定义一下迭代器。迭代器开始位置就是返回哨兵位头节点的下一个位置,结束位置就是返回哨兵位的地址。

测试一下:

void test_list1(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);list<int>::iterator it = lt.begin();while (it != lt.end()){//*it += 20;cout << *it << " ";++it;}cout << endl;
}

测试结果:
在这里插入图片描述
有了迭代器就有范围for:

for (auto e : lt)
{cout << e << " ";
}
cout << endl;

在这里插入图片描述
总结:其实会发现就是在模拟指针,但他的底层细节很大。所以迭代器体现了封装的强势之处。封装屏蔽底层差异和实现细节,提供统一的访问修改遍历方式。这样我们就不用关注他的底层是什么.
在这里插入图片描述

举个例子:

	set<int> s;s.insert(1);s.insert(3);s.insert(2);s.insert(4);set<int>::iterator sit = s.begin();while (sit != s.end()){cout << *sit << " ";++sit;}cout << endl;
}

在这里插入图片描述
这里的set就是树,我们也可以依然用这个迭代器进行遍历。

实现迭代器++,就是指针往前走的过程,注意返回的是迭代器。

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

后置++与后置–

//后置
self operator++(int)
{self tmp(*this);_node = _node->_next;return tmp;
}
//后置
self operator--(int)
{self tmp(*this);_node = _node->_prev;return tmp;
}

->

在讲->重载之前,先看一下这个示例:

struct AA
{AA(int a1 = 0, int a2 = 0):_a1(a1), _a2(a2){}int _a1;int _a2;
};
list<AA> lt;
lt.push_back(AA(1, 1));
lt.push_back(AA(2, 2));
lt.push_back(AA(3, 3));list<AA>::iterator it = lt.begin();
while (it != lt.end())
{cout << *it << endl;++it;
}
cout << endl;

在这里就访问不了,因为自定义类型不支持类型。

这里我们回顾一下之前的知识,对与内置类型的指针,我们可以采用*来进行解引用。对于自定义类型的指针,我们要用->来进行解引用。

int* p = new int;
*p = 1;AA* ptr = new AA;
ptr->_a1 = 1;

实现->

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

==

两个迭代器进行比较,就是两个指针比较

bool operator==(const self& s)
{return _node == s._node;
}

const迭代器

在实现const迭代器之前,首先要知道一点,const迭代器是一个完全不一样的类,所以不能将非const迭代器前加const就变成const迭代器。
在这里插入图片描述
因此我们可以list类中在定义一个const迭代器:

typedef __list_const_iterator<T> const_iterator;
const_iterator begin() const
{return _head->_next;
}
const_iterator end() const
{return _head;
}

在单独实现一个const迭代器的类:

template<class T>
struct __list_const_iterator
{....
}

const迭代器基本的功能与非const迭代器相似,只有在解引用时不同:

// *it = 10
const T& operator*()
{return _node->_data;
}// it->a1 = 10
const T* operator->()
{return &_node->_data;
}

测试一下:

void print_list(const list<int>& lt)
{list<int>::const_iterator it = lt.begin();while (it != lt.end()){//*it = 10;cout << *it << " ";++it;}cout << endl;for (auto e : lt){cout << e << " ";}cout << endl;
}void test_list4()
{list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);print_list(lt);
}

在这里插入图片描述
但是这样实现,还是太冗余了,因为非const和const只有返回值不同,那么还有优化的空间吗?
我们看一下库里面的实现:
在这里插入图片描述
库里面定义只定义了同一个类模版的迭代器,只是这两个迭代器之间的摸版参数不同,实例化的参数不同,就是完全不一样的类型。也就是说把能靠模版实现的就写一份,让编译器搞。

所以我们可以将我们的迭代器进行进一步优化:

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;....
}
// T T& T*
// T cosnt T& const T*
template<class T, class Ref, class Ptr>
struct __list_iterator
{typedef list_node<T> Node;/*typedef __list_iterator<T> self;*/typedef __list_iterator<T, Ref, Ptr> self;Node* _node;...
}

到这里,我们的迭代器就全部实现完了。

拓展:

在刚才的测试函数中,有一个print_list函数,但是这个测试函数里面的数据我们给定的是int,那我要是其他类型的呢,这个函数又该如何修改呢?
其实很简单:我们加一个摸版参数即可:

template<typename T>
void print_list(const list<T>& lt)
{typename list<T>::const_iterator it = lt.begin();while (it != lt.end()){//*it = 10;cout << *it << " ";++it;}cout << endl;
}

测试一下:
在这里插入图片描述
注意:这里我们没有用class这个摸版参数,这是因为:

list是一个未实例化的类模板,编译器不能去他里面去找
编译器就无法确定:list::const_iterator是内嵌类型,还是静态成员变量
前面加一个typename就是告诉编译器,这里是一个类型,等list实例化后再去类里面去取

拓展2:

如果要是将刚才的类在改造一下呢?
比如:
我要打印以下内容:

vector<string> v;
v.push_back("222222222222222222222");
v.push_back("222222222222222222222");
v.push_back("222222222222222222222");
v.push_back("222222222222222222222");

这个函数对于我们的printf_list就不适用了,因为我们的print_list就只适用于链表。
这里我们就可以写一个容器(container)的打印函数:

template<typename Container>
void print_Container(const Container& con)
{typename Container::const_iterator it = con.begin();while (it != con.end()){cout << *it << " ";++it;}cout << endl;
}

测试结果:
在这里插入图片描述
总结一下:
摸版实现了泛型编程,而泛型编程的本质,就是本来我们干的活,交给了编译器。

相关函数接口:

有了迭代器的实现,我们就可以实现一下链表的相关接口:

Insert:

Insert:在pos位置之前插入:

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

Insert迭代器不会产生失效的问题,因为没有扩容。

erase:

在指定位置删除:

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

注意:erase的迭代器会失效,所以我们加个返回值。

实现了insert和erase后,我们就可以服用来实现头插,头删,尾插,尾删。

push_front与pop_fronr

具体实现:

头插:

//头插
void push_front(const T& x)
{insert(begin(), x);
}

头删:

//头删
void pop_front()
{erase(begin());
}

push_back与pop_back

具体实现:

尾插:

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

尾删:

//尾删
void pop_back()
{erase(--end());
}

size:

为了方便计算大小,我们还可以再实现一个函数:

size_t size()
{return _size;
}

clear与析构:

clear:清理空间,我们可以采取迭代器访问的方式,逐个将节点释放。

//清理空间:
void clear()
{iterator it = begin();while (it != end()){it = erase(it);}
}

析构:我们可以先清理空间,在将头节点释放即可。

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

拷贝构造:

我们可以采用范围for来进行拷贝构造:

//拷贝构造:
// lt2(lt1)
//list(const list<T>& lt)
list(list<T>& lt)
{empty_init();for (auto e : lt){push_back(e);}
}

赋值重载:

传统写法:

list<int>& operator=(const list<int>& lt)
{if (this != &lt){clear();for (auto e : lt){push_back(e);}}return *this;
}

现代写法:

void swap(list<T>& lt)
{std::swap(_head, lt._head);std::swap(_size, lt._size);
}// lt3 = lt1
list<int>& operator=(list<int> lt)
{swap(lt);return *this;
}

对比vector与list

在这里插入图片描述

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

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

相关文章

【深度学习笔记】03 微积分与自动微分

03 微积分与自动微分 导数和微分导数解释的可视化偏导数梯度链式法则自动微分非标量变量的反向传播分离计算 导数和微分 假设我们有一个函数 f : R → R f: \mathbb{R} \rightarrow \mathbb{R} f:R→R&#xff0c;其输入和输出都是标量。 如果 f f f的导数存在&#xff0c;这个…

GIT版本控制和常用命令使用介绍

GIT版本控制和常用命令使用介绍 1. 版本控制1.1 历史背景1.2 什么是版本控制1.3 常见版本控制工具1.4 版本控制的分类 2 Git介绍2.1 Git 工作流程2.2 基本概念2.3 文件的四种状态2.4 忽略文件2.5 Git命令2.5.1 查看本地git配置命令2.5.2 远程库信息查看命令2.5.3 分支交互命令2…

WorkPlus即时通讯软件,以自主安全为底座,连接工作的一切

在当今竞争激烈的商业环境中&#xff0c;中大型企业对于移动办公平台的需求越来越迫切。在众多可选的平台中&#xff0c;WorkPlus凭借其高性价比和针对中大型企业的特色功能&#xff0c;成为了许多企业的首选。本文将为各位读者深度解析WorkPlus私有化部署的优势&#xff0c;带…

学习.NET验证模块FluentValidation的基本用法(续2:其它常见用法)

FluentValidation模块支持调用When和Unless函数设置验证规则的执行条件&#xff0c;其中when函数设置的是满足条件时执行&#xff0c;而Unless函数则是满足条件时不执行&#xff0c;这两个函数的使用示例如及效果如下所示&#xff1a; public AppInfoalidator() {RuleFor(x>…

Mysql 解决Invalid default value for ‘created_at‘

在mysql版本 8.0 和 5.* 之间数据互导的过程中&#xff0c;老是会出现各种错误&#xff0c;比如 这个created_at 一定要有一个默认值&#xff0c; 但是我加了 default null 还是会报错&#xff0c;于是对照了其他的DDL 发现&#xff0c;需要再加 null default null 才行&#…

从0开始学习JavaScript--JavaScript事件:响应与交互

JavaScript的事件处理是Web开发中至关重要的一部分&#xff0c;通过事件&#xff0c;能够实现用户与页面的互动&#xff0c;使得网页更加生动和交互性。本文将深入探讨JavaScript事件的各个方面&#xff0c;包括事件的基本概念、事件类型、事件对象、事件冒泡与捕获、事件委托、…

如何看待 2023 OPPO 开发者大会?潘塔纳尔进展如何?AndesGPT 有哪些亮点?

在2023年11月16日举行的OPPO开发者大会&#xff08;ODC23&#xff09;上&#xff0c;OPPO带来了全新ColorOS 14、全新互联网服务生态以及健康服务进展&#xff0c;这些新动态中有许多值得关注的地方。 1、全新ColorOS 14&#xff1a; 效率提升&#xff1a;ColorOS 14通过一系列…

虚拟机可ping树莓派树莓派无法ping虚拟机 的解决办法

问题描述 在学习交叉编译的过程中&#xff0c;发现了树莓派无法ping通虚拟机的问题。所以我尝试了各种ping&#xff0c;发现&#xff1a; 虚拟机可以ping通树莓派和主机树莓派可以ping通主机主机可以ping通树莓派和虚拟机唯独树莓派没法ping通虚拟机 尝试各种方法后找到一种…

Qt手写ListView

创建视图&#xff1a; QHBoxLayout* pHLay new QHBoxLayout(this);m_pLeftTree new QTreeView(this);m_pLeftTree->setEditTriggers(QAbstractItemView::NoEditTriggers); //设置不可编辑m_pLeftTree->setFixedWidth(300);创建模型和模型项&#xff1a; m_pLeftTree…

车载通信架构 —— 传统车内通信网络FlexRay(较高速度高容错、较灵活拓扑结构)

车载通信架构 —— 传统车内通信网络FlexRay(较高速度高容错、较灵活拓扑结构) 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,…

如何在3dMax中根据AutoCAD地形规划文件对地形进行建模?

在3dMax中根据Autocad地形规划文件对地形进行建模的方法 直入主题&#xff0c;要根据包含地形图的DWG (Autocad) 文件进行地形建模&#xff0c;方法步骤如下&#xff1a; 1.运行3dmax软件&#xff0c;点击“文件&#xff08;File&#xff09;->导入&#xff08;Import&…

用友NC word.docx接口存在任意文件读取漏洞 附POC

@[toc] 用友NC word.docx接口存在任意文件读取漏洞 附POC 免责声明:请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失,均由使用者本人负责,所产生的一切不良后果与文章作者无关。该文章仅供学习用途使…

使用Python的turtle库绘制随机生成的雪花

1.1引言 在这篇文章中&#xff0c;我们将使用Python的turtle库来绘制一个具有分支结构的雪花。该程序使用循环和随机颜色选择来绘制20个不同大小和颜色的雪花。turtle库是一个流行的绘图库&#xff0c;常用于创建图形用户界面和简单的动画。这个代码实现了一个有趣的应用&…

Elasticsearch:ES|QL 查询中的元数据字段及多值字段

在今天的文章里&#xff0c;我来介绍一下 ES|QL 里的元数据字段以及多值字段。我们可以利用这些元数据字段以及多值字段来针对我们的查询进行定制。 ES|QL 源数据字段 ES|QL 可以访问元数据字段。 目前支持的有&#xff1a; _index&#xff1a;文档所属的索引名称。 该字段的…

vue2项目从0搭建(三):配置环境变量及对应的webpack配置

前言 实际业务开发中,一个项目很可能会同时配置好几套环境。 比如:常规开发环境,开发测试环境,正式的测试环境,预发测试环境,客户甲的生产环境,客户乙的生产环境,通用生产环境,独立应用环境,微前端环境,大屏专用环境,移动端环境。 一女多嫁的实际业务场景,就需要我们进行多样…

Android 提示框代码 java语言

在Android中&#xff0c;你可以使用 AlertDialog 类来创建提示框。以下是一个简单的Java代码示例&#xff0c;演示如何创建和显示一个基本的提示框&#xff1a; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; im…

RocketMq架构和源码解析

NameServer&#xff1a;作为注册中心&#xff0c;提供路由注册、路由踢出、路由发现功能&#xff0c;舍弃强一致&#xff0c;保证高可用&#xff0c;集群中各个节点不会实时通讯&#xff0c;其中一个节点下线之后&#xff0c;会提供另外一个节点保证路由功能。 Broker&#xf…

vatee万腾的科技征途:Vatee独特探索的数字化力量

在数字化时代的浪潮中&#xff0c;Vatee万腾以其独特的科技征途成为引领者。公司在数字化领域的探索之路不仅是技术的创新&#xff0c;更是一种对未知的勇敢涉足&#xff0c;是对新时代的深刻洞察和积极实践。 Vatee万腾通过独特的探索&#xff0c;展示了在数字化征途上的创新力…

Matplotlib子图的创建_Python数据分析与可视化

Matplotlib子图的创建 plt.axes创建子图fig.add_axes()创建子图 plt.axes创建子图 前面已经介绍过plt.axes函数&#xff0c;这个函数默认配置是创建一个标准的坐标轴&#xff0c;填满整张图。 它还有一个可选的参数&#xff0c;由图形坐标系统的四个值构成。这四个值表示为坐…

Spine深入学习 —— 数据

atlas数据的处理 作用 图集&#xff0c;描述了spine使用的图片信息。 结构 page 页块 页块包含了页图像名称, 以及加载和渲染图像的相关信息。 page1.pngsize: 640, 480format: RGBA8888filter: Linear, Linearrepeat: nonepma: truename: 首行为该页中的图像名称. 图片位…