C++STL的list模拟实现

文章目录

    • 前言
  • list实现
    • push_back
    • 迭代器(重点)
      • 普通迭代器
      • const迭代器
    • insert
    • erase
    • 析构函数
    • 构造函数
    • 拷贝构造
    • 赋值
  • vector和list的区别

前言

要实现STL的list, 首先我们还得看一下list的源码。
在这里插入图片描述
我们看到这么一个东西,我们知道C++兼容C,可以用struct来创建一个类。但是我们习惯用class。

那什么时候会用struct呢?
这个类所有成员都想开放出去,比如结点的指针,它一般开放出来。所以我们用struct.。

继续看源码比较重要的东西,成员变量的结构。
在这里插入图片描述

这个东西是啥?
在这里插入图片描述
在这里插入图片描述
这样就很清晰了。

知道它是一个结点的指针,下一步 应该看什么?
成员看了,就看接口。
看接口第一步,看构造函数,看构造函数就知道它怎样初始化,就知道它的初始结构是怎样的。
初始结构摸清楚了,就对它的大概形态摸清楚了。

接着看它的核心方法,当然我们本身对list有一定的了解。
头插头删,尾插尾删就是核心方法。

看它的构造函数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
后面就先不接着往下看了。

list实现

先把最基本的东西写出来。

namespace but
{template<class T>struct list_node{list_node<T>* _next;list_node<T>* _prev;T _data;};template<class T>class list{list(){_head = new list_node;_head->_next = _head;_head->_prev = _head;}private:list_node* _head;};
}

push_back

在这里插入图片描述
在这里插入图片描述

为什么报错?
在这里插入图片描述
前面我们说过像构造函数,参数可以不加模板参数,但是声明类型还是得加上。

list_node是类名,list_node才是类型。

更新一下前面的代码。

namespace but
{template<class T>struct list_node{list_node<T>* _next;list_node<T>* _prev;T _data;};template<class T>class list{typedef list_node<T> node;list(){_head = newnode;_head->_next = _head;_head->_prev = _head;}private:node _head;};
}

push_back怎么搞?
找到尾,然后new 一个新节点,最后链接。
在这里插入图片描述

void push_back(const T& x)
{node* tail = _head->_prev;node* new_node = new node(x);tail->_next = new_node;new_node->_prev = tail;new_node->_next = _head;_head->_prev = new_node;
}

写个list_node的构造函数。

list_node(const T& x ):_next(nullptr), _prev(nullptr), _data(x)
{}

紧接着报错。
在这里插入图片描述
没有默认构造怎么办?
最好还是提供一个全缺省的构造函数。

//list_node(const T& x =0)不能给0
list_node(const T& x =T()):_next(nullptr), _prev(nullptr), _data(x)

迭代器(重点)

普通迭代器

首先我们肯定会遇到一个问题,之前的vector的数据是连续存放的,而链表每个结点是不连续的。
++不能指向下一个结点。
在这里插入图片描述
怎么解决这个问题?
能不能给node提供一个重载,不行,因为是node*而不是node;

我们可以看一下STL的源码。
在这里插入图片描述
++还可以解引用
在这里插入图片描述

现在我们根据自己的理解,写一个简单的迭代器,让它运行起来。
在这里插入图片描述

接着我们再在list这个对象里写上begin()和end()就可以正常访问了。
在这里插入图片描述

最后测试一下
在这里插入图片描述
在这里插入图片描述
大家仔细看,数组和链表的结构千差万别,但是用起来是如此的相似。
这源自于封装,屏蔽掉了我们看不到的细节。

今天最重要的并不是链表的实现,迭代器的实现才是最最重要的。

总结一下,node*不支持解引用,不支持++,但是我可以用一个自定义类型对你封装,然后去重载运算符,我可以控制我想要的解引用的行为,想要的++的行为,这是自定义类型达到的意义。

在这里插入图片描述
注意看这里有个隐藏的点,发生了拷贝构造,我们自己没有写拷贝构造,编译器自动生成的不会 出问题吗?
在这里插入图片描述
程序运行没有报错,什么原因呢?这里没有写析构函数,不需要释放结点。

为什么不需要释放结点?
虽然有结点的指针,但是这结点的指针并不属于迭代器。
结点的指针给迭代器,只是为了遍历链表,++,解引用,修改链表。
释放是链表的事情,链表的析构函数会释放,不需要你释放。
这个结点不是迭代器new出来的,你只有使用权,没有归属权。

template<class T, class Ref, class Ptr>
struct __list_iterator
{typedef list_node<T> node;typedef __list_iterator<T, Ref, Ptr> self;node* _node;__list_iterator(node* n):_node(n){}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;}
};

const迭代器

假设我们传了const的链表,编译不通过。
在这里插入图片描述
为什么编译不通过?
还是我们之前讲了很多次的权限放大。
我们提供一个支持const对象的迭代器就可以了。

但是看这里,为什么const对象还可以调用构造迭代器?
在这里插入图片描述
首先const修饰的*this具体是_head;所以_head不能被改变,而不是_head指向的内容不能被改变。
这个结点指针本身不能改变,但是它可以拷贝给别人。

但是这样写不符合我们的预期,可以修改了。为什么能修改呢?就是因为它构造出了普通迭代器。但是普通迭代器是不可写的。

我们要写一个const迭代器

首先我们先想一下普通迭代器和const迭代器的区别是什么?

先看一个问题,能不能这样定义const迭代器?
在这里插入图片描述
绝对不可以。
首先迭代器对标的是指针。
在这里插入图片描述

写成上面这样,是保护迭代器本身不能修改,而我们想要的是,迭代器指向的内容不能修改,也就是 const T*;

那怎么实现呢,我们要实现的内容不能修改。
我们可以像之前实现普通迭代器一样,再写一个const迭代器对象,只是名字改一下,然后解引用的时候不能修改。
在这里插入图片描述

在这里插入图片描述

两个对象除了那个返回值不一样,其他都一样怎么简化一下呢?
控制返回值不一样就可以了。增加一个模板参数。
还能这么玩。
在这里插入图片描述
在这里插入图片描述

// 1、迭代器要么就是原生指针
// 2、迭代器要么就是自定义类型对原生指针的封装,模拟指针的行为
template<class T, class Ref, class Ptr>
struct __list_iterator
{typedef list_node<T> node;typedef __list_iterator<T, Ref, Ptr> self;node* _node;__list_iterator(node* n):_node(n){}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;}};

我们看一下库里面的模板参数
在这里插入图片描述

为什么还有一个Ptr呢?
它还提供了一个重载operator->;
在这里插入图片描述
什么时候会用->?
大家注意,上面的迭代器模拟的是int*;
自定义的类型是不是得用->
在这里插入图片描述
在这里插入图片描述
大家看报错了。报的啥错。
it返回的是AA,AA没有返回流插入。

第一种方式可以使用重载一个流插入,这里因为AA里面的成员都不是私有,所以我们可以这样。
在这里插入图片描述
这样写很别扭我们可以这样。
在这里插入图片描述
我们可以在迭代器里面重载一个->
在这里插入图片描述

总感觉有点怪怪,其实是这样的。
在这里插入图片描述
在这里插入图片描述

好,接下来const的迭代器的->重载需要返回const T*,所以这里再增加一个模板参数。

insert

链表其实已经实现的差不多了,我们现在自己再把功能完善一下。其实我们没必要实现头插头删尾插尾删,我们只需要实现insert.和 erase, insert和erase实现了,其他都可以实现。
在这里插入图片描述

void insert(iterator pos, const T& x)
{node* cur = pos._node;node* prev = cur->_prev;node* new_node = new node(x);prev->_next = new_node;new_node->_prev = prev;new_node->_next = cur;cur->_prev = new_node;
}

链表的insert会不会导致迭代器失效?
不会
因为pos始终指向这个结点,并且这个位置关系也不会变。

接着我们其实自己不用写push_back和push_front了

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

erase

在这里插入图片描述

void erase(iterator pos)
{
//哨兵卫头节点不能删除assert(pos != end());node* prev = pos._node->_prev;node* next = pos._node->_next;prev->_next = next;next->_prev = prev;delete pos._node;
}

链表的erase会不会导致迭代器失效?
铁铁的失效,迭代器指向的结点的指针都被干掉了

void pop_back()
{erase(--end());
}void pop_front()
{erase(begin());
}

在这里插入图片描述
大家看下面这两行代码的差异在哪里?
本质上没有差异。它们的差异点在于pnode是一个内置类型,it是一个自定义类型。

从物理空间上看,它们的代码是一摸一样的,都是4个字节,并且都是同一个地址。
在这里插入图片描述
但是这两个的行为天差地别

在这里插入图片描述
这就是C语言和C++的差异。

析构函数

clear可以帮我们把数据清掉,但是它不清头结点。

void clear()
{iterator it = begin();while (it != end()){it = erase(it);//防止迭代器失效erase(it++);}
}

在这里插入图片描述
这样写行不行?
可以。it不是失效了吗?为什么还可以it++; 我们之前说过it失效有个现象就是野指针,那这里怎么没事呢?
这就是后置++的价值,它会返回++之前的值。
在这里插入图片描述
也就是说erase的并不是it,而是返回的迭代器。

析构和clear的区别就是头节点要不要清楚掉,析构是彻底不用了。

~list()
{clear();delete _head;_head = nullptr;
}void clear()
{iterator it = begin();while (it != end()){//it = erase(it);erase(it++);}
}

构造函数

我们再提供一下迭代器区间的构造。
在这里插入图片描述
这样写可不可以,不可以,你要push_back,你得有一个哨兵卫的头节点。

void empty_init()
{_head = new node;_head->_next = _head;_head->_prev = _head;
}template <class Iterator>
list(Iterator first, Iterator last)
{empty_init();while (first != last){push_back(*first);++first;}
}

const对象可不可以调用构造函数。可以。
在这里插入图片描述

拷贝构造

传统写法
在这里插入图片描述

现代写法

void swap(list<T>& tmp)
{std::swap(_head, tmp._head);
}list(const list<T>& lt)
{empty_init();list<T> tmp(lt.begin(), lt.end());swap(tmp);
}

在这里插入图片描述
this跟tmp交换,但是this是随机值,会报错,所以要初始化。

赋值

在这里插入图片描述
为什么不用引用传参?
用引用会导致一个非常恶劣的后果。
大家看,传引用的话,lt就是lt3,交换就变成lt1和lt3的交换了。


// lt1 = lt3
list<T>& operator=(list<T> lt)
{swap(lt);return *this;
}

vector和list的区别

其实就是顺序表和链表的区别。

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

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

相关文章

保姆级:Windows Server 2012上安装.NET Framework 3.5

&#x1f4da;&#x1f4da; &#x1f3c5;我是默&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; ​​ &#x1f31f;在这里&#xff0c;我要推荐给大家我的专栏《Windows》。&#x1f3af;&#x1f3af; &#x1f680;无论你是编程小白&#xff0c;还是有…

【python】PDF转长图

PDF转化成长图 步骤&#xff1a; pip install pdf2image下载依赖文件poppler 上代码 # 导入所需的库 from PIL import Image import fitz from pdf2image import convert_from_path# 定义PDF文件路径、保存图像路径和Poppler路径 pdf_file rD:\workspace\python学习笔记.pdf…

什么是产品经理 | 文末赠书

目录 一. 产品经理是什么&#xff1f;二. 产品经理需要具备的技能三. 产品经理的职责四. 产品经理在软件开发过程中如何平衡不同利益方的需求&#xff1f;五. 入门建议六. 发展前景七. 总结&#x1f981;文末福利图书介绍作者简介 一. 产品经理是什么&#xff1f; 产品经理是指…

javascript代码规范

语句规范 所有语句后面需要有";"(*)If、for、do、while 等所有循环体和判断体的执行语句部分都用“{}”括起来&#xff0c;尽量不要省略花括号(不强制,单行时可省略)。禁止在 return 关键字及要返回的表达式之间换行(*)。每行代码应少于 120 个字符&#xff0c;多于…

程序员保密协议(软件开发岗位协议)

保密协议 甲方&#xff1a; 乙方&#xff1a; 鉴于&#xff1a;乙方愿意被聘用到甲方工作&#xff0c;双方经平等协商一致&#xff0c;自愿达成本保密协议。本协议涉及保密和知识产权的内容。 本协议所称任职期间&#xff0c;是指自甲乙双方劳动…

【flink番外篇】3、flink的source(内置、mysql、kafka、redis、clickhouse)介绍及示例(4)- redis -异步读取

Flink 系列文章 一、Flink 专栏 Flink 专栏系统介绍某一知识点&#xff0c;并辅以具体的示例进行说明。 1、Flink 部署系列 本部分介绍Flink的部署、配置相关基础内容。 2、Flink基础系列 本部分介绍Flink 的基础部分&#xff0c;比如术语、架构、编程模型、编程指南、基本的…

四、mapbox搭载vue3测试demo(31-40)

demo地址https://bidding-m.gitee.io/mapbox-test/#/ 31、[添加] 热力图图层 32、[添加] 样式聚类 33、[添加] HTML聚类 34、[添加] 点动画效果 35、[添加] marker动

探索SSL证书的应用场景,远不止网站,还有小程序、App Store等

说到SSL证书&#xff0c;我们都知道其是用于实现HTTPS加密保障数据安全的重要工具&#xff0c;在建设网站的时候经常会部署SSL证书。但实际上&#xff0c;SSL证书的应用场景远不止网站&#xff0c;它还被广泛地应用到小程序、App Store、抖音广告、邮件服务器以及各种物联网设备…

用户管理第2节课 -- idea 2023.2 创建表

一、懂得 1.1编码格式是防止乱码的&#xff0c;utf-8是完全够的&#xff0c;那几个基本没差别 网址&#xff1a; 【IDEA——连接MySQL数据库&#xff0c;创建库和表】_idea中数据库-CSDN博客 这些是MySQL数据库中的一些术语&#xff0c;可以简单解释如下&#xff1a; 1、col…

【K8S 系列】认识k8s、k8s架构

一、什么是k8s? Kubernetes 简称 k8s&#xff0c;是支持云原生部署的一个平台&#xff0c;k8s 本质上就是用来简化微服务的开发和部署的&#xff0c;用于自动化部署、扩展和管理容器化应用的开源容器编排技术。对于传统的docker其实也提供了容器编排的技术docker-compose&…

SystemServer 进程启动过程

首语 SystemServer进程主要用于启动系统服务&#xff0c;诸如AMS、WMS、PMS都是由它来创建的。在系统的名称为"system_server"&#xff0c;Android核心服务都是它启动&#xff0c;它是非常重要。 Zygote处理SystemServer进程 在 Zygote启动过程 文章中分析我们知道…

LeetCode算法练习:双指针计算三数之和和四数之和

通过双指针将时间复杂度降一个级别。 public class TOP {//15. 三数之和public List<List<Integer>> threeSum(int[] nums) {List<List<Integer>> res new ArrayList<>();int n nums.length;if (n < 3) {return res;}Arrays.sort(nums);//…

2023/12/12作业

思维导图 作业&#xff1a; 成果图 代码 #include "widget.h" #include "ui_widget.h" Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { speechernew QTextToSpeech(this); ui->setupUi(this); //一直获取当前时间 idst…

SQL server创建联合索引

CREATE INDEX idx_dim_product_dt_ord ON [dim_product] (dt, ord); 在SQL Server中计算月同比和季度同步的SQL查询可能看起来像这样&#xff1a; ### 月度同比 月度同比是与前一年同一月份的数据进行比较。以下是一个基本的例子&#xff0c;假设我们有一个名为sales的表&…

1843_emacs中两个插件use-package以及org-bullets的使用

Grey 1843_emacs中两个插件use-package以及org-bullets的使用 全部学习汇总&#xff1a; GitHub - GreyZhang/editors_skills: Summary for some common editor skills I used. 我个人的emacs的配置以及两个插件的使用由来 我自己现在也开始维护一个我自己的emacs配置&…

剑指offer(C++)-JZ49:丑数(算法-其他)

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 题目描述&#xff1a; 把只包含质因子2、3和5的数称作丑数&#xff08;Ugly Number&#xff09;。例如6、8都是丑数&#xff0c;…

深入理解Golang中的goroutine 池

并发性是 Golang 的一项强大功能,它允许开发人员同时有效地管理多个任务。工作线程池的实现是并发的最常见用例之一。在本文中,我们将了解 Golang 中工作线程池的概念,讨论它们的好处,并引导您完成在下一个 Go 项目中实现工作线程池的过程。 什么是工作线程池? 工作线程…

【数据库】基于有效性确认的并发访问控制原理及调度流程,乐观无锁模式,冲突较少下的最优模型

使用有效性确认的并发控制 ​专栏内容&#xff1a; 手写数据库toadb 本专栏主要介绍如何从零开发&#xff0c;开发的步骤&#xff0c;以及开发过程中的涉及的原理&#xff0c;遇到的问题等&#xff0c;让大家能跟上并且可以一起开发&#xff0c;让每个需要的人成为参与者。 本专…

物流供应链数字化转型:国内领先服务商技术综合解析

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

TCP/IP详解——IP协议,IP选路

文章目录 1. IP 编址1.1 IP 报文头部1.2 进制之间的转换1.3 网络通信1.4 有类 IP 编制的缺陷1.5 变长子网掩码1.6 网关1.7 IP 包分片1.7.1 IP 包分片实例1.7.2 IP 分片注意事项1.7.3 Wireshark 抓取 IP 包分片1.7.4 OmniPeek 抓取 IP 包分片1.7.5 ICMP 不可达差错&#xff08;需…