[c++]list模拟实现

news2024/11/27 21:08:29

目录

前言:

学习类的方式:

1 类成员变量

1.1 list成员变量

 1.2 结点结构体变量

1.3 迭代器成员变量

2 默认函数——构造

2.1 结点结构体构造函数

2.2 list构造函数

2.3 迭代器构造函数

3 迭代器实现

3.1 list部分

3.2 迭代器结构体部分

3.2.1 operator*

3.2.2 operator++/operator--

3.2.3 operator->

3.2.4 operator==/!=

3.2.5 Ref和Ptr

代码:

4 区间构造、拷贝、赋值、析构

4.1 迭代器区间构造

4.2 拷贝构造

4.3 赋值重载

4.4 析构函数

5 插入删除函数


前言:

        本篇模拟实现了STL中的list容器,其中的难点不在于其数据结构——链表,而在于对于类与类之间的联系和互相调用,关系十分巧妙,并且还有对于模板类的牛逼写法,相信大家认真阅读之后一定会有一些收获的。

学习类的方式:

        如果大家学过string或者vector等其它类后一定会养成一个好习惯——了解一个类必须先了解它的成员变量,然后就是它的默认函数,再然后就是迭代器、最后才是它的其它函数功能

1 类成员变量

1.1 list成员变量

        首先看到list类的成员变量,没错,你没有看错只有一个_head,但是它的类型是Node*,通过我们typedef出来的类型,看到这个有没有联想到以前在哪里见到过吗?Linux中的string类当中就是通过这样的方式,用一个指针将一个结构类型封装起来,而不是像我上篇vector中使用的哪种方式,将一个结构用拆分成为一个个的变量作为list类的成员,虽然实际底层上没有任何的区别,但是对于代码的独有性就变得不在健全,还有很多功能是不好实现的,这一点我之后迭代器讲解。

 template<class T>
class list

{

public:

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

private:

        Node* _head;

}        

 1.2 结点结构体变量

        如果大家有链表的基础的话,我想应该是能够反应过来这个链表的形式是一个双向的,因为他保留了前后结点的指针,所以能够前移或者后移,不过因为我们想要一个非常高效的链表,单纯的双向还不够,我们还需要添加上循环属性,成为链表中最牛逼的带头双向循环链表

        其中的T类型是模板类型,不单纯指代一个类型,也就是我们可以构造出存储能够任意类型的链表,包括自定义类型。

template<class T>
struct node
{

        struct node<T>* next;
        struct node<T>* prev;
        T data;

};

1.3 迭代器成员变量

        要说STL容器当中的什么设计最牛逼,我只会给出一个答案,那就是迭代器思想的构建,要问为什么?只能说通过对数据结构封装成为的迭代器,让外部接口在不知道这个结构是如何的也能够通过相同的接口访问起来,极大的节省了初学者的精力。

        举一个例子:(看下方的代码)

void test1()
{
	std::vector<int> vv = { 1, 2, 3, 4, 5 };
	std::list<int> ll = { 1, 2, 3, 4, 5 };
	std::vector<int>::iterator itv = vv.begin();
	while (itv != vv.end())
	{
		std::cout << (*itv *= 2) << " ";
		itv++;
	}
	std::cout << std::endl;

	std::list<int>::iterator itl = ll.begin();
	while (itl != ll.end())
	{
		std::cout << (*itl *= 3) << " ";
        itl++;
	}
    std::cout << std::endl;
}

         相信学过数据结构的大伙一定是明白vector和list的实现底层是完全不一样的,但是呢,大家发现了没有,这两个底层结构完全不同的数据结构被迭代器组织,将两个数据结构的使用变成了统一结构。是不是感觉很牛逼?只能说祖师爷和大佬们的世界太大了,要是本博主能够想出这种结构,估计这辈子的牛都够吹了。哈哈。如下是访问结果。

         看到下方的迭代器结构体,其中也是只有一个变量,并且与我们的list类的变量类型完全一致,不过大家不用感到惊奇,因为迭代器类的出现本身就是为了辅助list访问结点的类的成员,所以类型相等是必然的,并且提前告诉你们,它们不只是类型相等,而且变量指向的空间也是同一份。那么这样做不会导致空间重复释放吗?

        答案是,一定不会,除非咱们脑子突然被狗啃了,要在迭代器结构体当中写一个析构函数与给这片空间释放了。那么这也就表示了,迭代器是不需要拷贝构造和赋值重载的,我们要的就是浅拷贝

        迭代器表示:我只是摸摸,看看,动动,但是绝对不会给你顺了,放一百个心。

template<class T, class Ref, class Ptr>
struct __list_iterator
{         

     typedef struct node<T> Node;
     typedef struct __list_iterator<T, Ref, Ptr> self;

     Node* Pos;
};

        至于迭代器模板中的Ref和Ptr大家现在不用理会,等到之后我会为大家解释。

2 默认函数——构造

2.1 结点结构体构造函数

        构造当中,我使用了T()作为缺省参数,毕竟咱还是得考虑类型是自定义类型的情况,又或者是有帅小伙懒得都不想传参数了,比如博主本人。

        因为这个结点只是结点,所以他还没有与链表构建联系,所以他不指向任何其他节点,指向空,数据直接赋值就行。

node(const T& val = T())
       :next(nullptr)
       , prev(nullptr)
       , data(val)
{}

         我突然想到,大家会不会在构造node结点时,在里面开一个空间?应该不会吧,毕竟只要我们调用了这个函数,那么空间必然是已经存在了,无论是在堆当中开辟还是在栈上,我们只是将这个空间当中的变量赋值,所以是不需要开空间的。

2.2 list构造函数

        因为我们写的链表是带头的,所以说就算是无参构造也不是直接给_head置一个nullptr就行,也是需要开辟一个结点空间的,并且该节点的前后指针都指向自己

        并且看到我开空间的地方了吗?是因为_head需要指向一个这样的空间,并且不能因为函数退出就失效,那么必定就需要去堆上开空间,这里就会去调用Node的构造函数,那么就有地方承载Node变量的空间了,自然是不需要到Node里再去开空间。

void Init_head()
{
    _head = new Node;
    _head->next = _head;
    _head->prev = _head;
}

//无参
list()
{
    Init_head();
}

2.3 迭代器构造函数

        迭代器的构造函数就简单了,只需要传入一个Node*类型的指针过来,然后进行浅拷贝赋值给Pos就行,相信大家也不会在这里难住。

struct __list_iterator(Node* node)
     :Pos(node)
{}

3 迭代器实现

3.1 list部分

            对于迭代器来说,最基础的就是需要提供begin和end函数,这样我们的范围for才能被编辑器实现出来。

        而且大家有注意到吗?以往我们写迭代器都是直接返回一个指针,但是对于list还是这样吗?很明显不是,我们是通过拿到头结点、尾结点的指针,然后通过这个指针去调用迭代器的构造函数,再将这个结构体返回拿到

iterator begin()
{
	//进入迭代器类当中去,然后传入起始值的数据初始化
	return iterator(_head->next);
}
iterator end()
{
	//_head相当于循环里的迭代器最后一个数据的下一个
	//所以初始化就用_head去初始化
	return iterator(_head);
}
const_iterator begin() const
{
	return const_iterator(_head->next);
}
const_iterator end() const
{
	return const_iterator(_head);
}

3.2 迭代器结构体部分

3.2.1 operator*

        我们平时在使用迭代器获取数据的时候是怎么样的?是不是直接*iterator就能得到了?但是对于我们的list迭代器呢?直接*iterator能得到数据吗?很明显不行的,并且我们的iterator可不是指针哦,而是一个完整的结构,那么它本身*是没有任何意义的。所以正因如此,我们需要对迭代器重载一个*运算符,当我们用*iterator时能够得到它里面结点内的数据

        那么这个时候我提一个问题,如果list类不封装迭代器,他还能像这样这样重载*吗?

        答案是不能,因为如果我们不封装迭代器,那么这个迭代器必然只能是一个指针,但是这个指针是我们node结点里数据的指针么?不是!它是这个结点的指针,用*解引用之后只能得到node结点这个结构体,而不是结构体里面的数据。

        那么有人又要问了,难道我得到了结点,我还不能访问数据了?

        可以,但是我问,我们通过迭代器拿到了结点,然后通过这个结点再去访问数据还是我们想要的结果吗?换句话说就是,迭代器的行为不再统一,我要他有什么用?如下:

//希望的结果

yf::list<int>::iterator it = head.begin();

*it = 数据

//不希望的结果

yf::list<int>::iterator it = head.begin();

*it = 结点

结点.data = 数据

        由此来看,list的迭代器封装起来并不只是为了实现功能分离,而是为了将行为统一,与其他容器的迭代器构成一个整体。 

3.2.2 operator++/operator--

        我们得知到迭代器++/--表示是什么意思,迭代器++/--指我们要将迭代器的结点走向想下个结点位置或者前一个结点位置,而不是走到连续空间的下一个位置。如果是vector这种形式的迭代器本身就是连续空间,那么它走到下一个位置自然是没有任何问题,但是关键是list不是,它的list被封装了,那么也就表示,迭代器只能重载++/--。根据前置后置需求不同,返回不同位置的结点。

3.2.3 operator->

        大家在使用一个结构体指针的时候会先解引用,然后再用这个结构体去访问里面的数据吗?这样说大家可能大家不是很理解,那么请看下方代码:

struct AA

{

        int a;

        int b;
}

struct AA val;

struct AA* ptr = &val;

//使用1

(*ptr).a = 2;

//使用2

ptr->a = 2;

        我不用脑子我都能知道大家使用结构体指针的时候必定是用第二种方式,用第一种不纯粹恶心自己吗,所以说迭代器也出现了这个东西->。当然这个->箭头运算符重载的不是我们的Node结点,而是Node结点里的变量data,如果它的类型是类、结构体,那么就能够使用。

list<struct AA> ll;

list<struct AA>::iterator it = ll.begin();

it->a = 2;

        也就是说,如果T类型本身就不支持使用->这个运算符,那么重载了->也是没有任何效果的。 

list<int> ll;

list<struct AA>::iterator it = ll.begin();

it-> //错误,都不知道你要指什么

//如果你要指数据直接*it就行哈

        还有大家有没有觉得这个重载少了点什么?

        机智的小伙估计已经看出来了,没错就是少了一个->符号,本来是:

list<struct AA> ll;

list<struct AA>::iterator it = ll.begin();

it->->a = 2;

         这样看不直观,但是展开就能知道了:

it.operator->()->a = 2;

        看出来了吗?本来应该有一个重载运算符的,但是这里上面的访问却没有了,为什么?

        编辑器省略了,为了代码的可读性。大佬们肯定是知道这里我们到底想要干嘛的,所以他们就对这种重定义做了处理,只用一个->就行。 

3.2.4 operator==/!=

        这两个重载很容易理解,你不是要比较这两个结点是不是相同吗?我又正好知道这两个结点的地址,那么只要地址不相同那就不相同,反之就相同。

3.2.5 Ref和Ptr

        看到这里大家应该也能知道为什么要有Ref和Ptr这两个模板参数的出现了吧,Ref为了区分const T& 和 T&,Ptr为了区分const T* 和 T*。

        为什么要区分呢?因为如果不区分的话我们就必须写两个内容功能基本相同的模板写两遍,那这个模板好垃圾哦,就出现了Ref,在模板里面又要考虑返回值是const T* 和 T* 这种类型,又要将函数写两边,这不纯恶心人吗,就有了Ptr。

代码:

//*迭代器返回结点内部的数据
Ref operator*()
{
	return Pos->data;
}

//到下一个结点,而不是结点指针的下一个位置
self& operator++()
{
	Pos = Pos->next;
	return *this;
}

self operator++(int)
{
	self temp = *this;
	Pos = Pos->next;
	return temp;
}

self& operator--()
{
	Pos = Pos->prev;
	return *this;
}

self operator--(int)
{
	self temp = *this;
	Pos = Pos->prev;
	return temp;
}

//返回节点数据的地址
Ptr operator->()
{
	//Pos已经是结点了,但是如果需要访问的数据也是一个结构体
	//那么获取到了它的地址,就能用->去访问了
	return &Pos->data;
}

bool operator==(const self& val)
{
	return Pos == val.Pos;
}

bool operator!=(const self& val)
{
	return Pos != val.Pos;
}

4 区间构造、拷贝、赋值、析构

4.1 迭代器区间构造

        迭代器区间可是一个好东西,它实现的可不仅仅是我们的同类型的迭代器去构造,他还能直接将其他类型的迭代器拿来拷贝,但是它本身的逻辑却很简单,如下。

//迭代器区间构造
template<class InputIterator>
list(InputIterator first, InputIterator last)
{
	Init_head();
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

4.2 拷贝构造

        我写的拷贝是现代写法,不是传统的每一个数据都要直接去拷贝,然后连接,那样太麻烦了,非常不符合我喜欢偷懒的习惯,所以代码如下:

//拷贝
list(const list<T>& val)
{
	Init_head();
	list<T> temp(val.begin(), val.end());
	std::swap(_head, temp._head);
}

        光看代码大家可能很疑惑,那么我就为大家解惑,首先看到Init_head(),这里是需要写的,不然之后交换函数时,_head是没有空间的,会使用野指针。我们的temp(val.begin(),val.end())这一句话直接通过原数据val拷贝出来了一个备份,而且不是指向同一个空间。

        也就表示了我们现在有三个头了,_head,temp._head,val._head,这个时候我们只需要将_head和temp._head交换一下,那么我们的类就有数据了,temp就没了。为什么能这么做?因为这些空间是在堆上的,不用担心除了函数作用域就消失了,消失的只是temp这个栈上的变量。这叫什么,窃取劳动法,十分牛逼,建议大家学习,哈哈。

4.3 赋值重载

        赋值重载和拷贝构造使用了同样的逻辑,都是偷,但是偷的方式不太一样,因为赋值重载的函数参数没有使用引用,那么传参过来回去调用拷贝构造,也就表示了已经有了新的空间,所以咱们直接去偷就可以。

//赋值重载
list<T>& operator=(list<T> val) 
{
	std::swap(_head, val._head);
	return *this;
}

4.4 析构函数

        析构函数是清理所有结点,clear函数是清理这个对象里除了头结点以外的所有结点,清理方式很简单,把节点delete掉就行,我的clear复用了我的erase函数,等下为大家分享。

//清空除头结点之外的所有结点
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		erase(it++);
	}
}

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

5 插入删除函数

        该代码完全与数据结构链表的实现逻辑相同,也太简单,所以博主不想讲解。

//插入
void insert(iterator pos, const T& val)
{
	Node* cur = pos.Pos;

	Node* NewNode = new Node(val);

	cur->prev->next = NewNode;
	NewNode->prev = cur->prev;
	NewNode->next = cur;
	cur->prev = NewNode;
}

//尾插
void push_back(const T& val)
{
	iterator it = end();
	insert(it,val);
}

//头插
void push_front(const T& val)
{
	iterator it = begin();
	insert(it,val);
}	

//删除
void erase(iterator pos)
{
	assert(pos != end());

	Node* cur = pos.Pos;

	cur->prev->next = cur->next;
	cur->next->prev = cur->prev;

	delete cur;
}

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

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

//判空
bool empty()
{
	return _head->next == _head;
}

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

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

相关文章

Spring MVC程序开发(三大功能)

文章目录一、什么是Spring MVC?1.MVC定义2.MVC与Spring MVC的关系3.创建方式二、Spring MVC的核心功能1.连接功能浏览器获取前端接口和后端程序连接功能实现get和post的区别Spring Boot热部署2.获取参数&#xff08;1&#xff09;传递单个参数&#xff08;2&#xff09;传递对…

信捷 XDH Ethercat A_MOVER指令

本指令以相对位置运动 如果当前位置在p点&#xff0c;执行本A_MOVER指令结束后&#xff0c;相对p点前移或后退一段距离&#xff0c;这个距离需要在指令里指定。每次执行A_MOVER都是执行前的当前位置为参考点&#xff0c;只要目标位置不为0&#xff0c;都会运动。上图中&#xf…

MyBatis-1:基础概念+环境配置

什么是MyBatis&#xff1f;MyBatis是一款优秀的持久层框架&#xff0c;支持自定义sql&#xff0c;存储过程以及高级映射。MyBatis就是可以让我们更加简单的实现程序和数据库之间进行交互的一个工具。可以让我们更加简单的操作和读取数据库的内容。MyBatis的官网&#xff1a;htt…

弱监督实例分割 Box-supervised Instance Segmentation with Level Set Evolution 论文笔记

弱监督实例分割 Box-supervised Instance Segmentation with Level Set Evolution 论文笔记一、Abstract二、引言三、相关工作3.1 基于 Box 的实例分割3.2 基于层级的分割四、提出的方法4.1 图像分割中的层级模型4.2 基于 Box 的实例分割在 Bounding Box 内的层级进化输入的数据…

elasticsearch 分布式搜索引擎3

1.数据聚合 **聚合&#xff08;aggregations&#xff09;**可以让我们极其方便的实现对数据的统计、分析、运算。例如&#xff1a; 什么品牌的手机最受欢迎&#xff1f;这些手机的平均价格、最高价格、最低价格&#xff1f;这些手机每月的销售情况如何&#xff1f; 实现这些…

Altium Designer(AD)软件使用记录04-AD设计文件输出汇总

目录Altium Designer(AD)软件使用记录04-AD设计文件输出汇总准备工作1、放置层标识&#xff08;标清每个层的顺序&#xff09;2、放置钻孔图&#xff08;表明孔的一些参数&#xff09;3、设置原点坐标一、文件输出1、Gerber文件&#xff08;光绘文件&#xff09;2、钻孔文件3、…

EasyExcell导出excel添加水印

EasyExcell导出excel添加水印1、添加easyExcel相关依赖2、准备基础工具类3、创建水印handler类4、创建单元测试类WriteTest.class5、测试结果1、添加easyExcel相关依赖 <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId&…

Reactor模式

Reactor是一种设计模式&#xff0c;可以用于构建高并发的网络服务器。 Reactor模式的好处在于&#xff1a;可以在一个或多个reactor线程使用多路复用技术去管理所有网络连接连接建立、IO请求&#xff0c;保证工作线程不被IO阻塞。 前置知识&#xff1a;IO多路复用技术 1. 传统网…

如何通过外贸网站SEO优化,提升您的品牌曝光度和海外业务销售额?

随着全球化的不断推进和互联网的普及&#xff0c;越来越多的外贸企业开始重视SEO优化&#xff0c;以提升自身品牌的曝光度和海外业务的销售额。 但是&#xff0c;对于许多外贸企业而言&#xff0c;SEO优化似乎是一个非常陌生的领域&#xff0c;他们并不清楚该如何进行优化。 …

两会特稿 | 项国就书法学术论文入编《中国民族博览》国家一级大型文化类期刊

2022 年 7 月&#xff0c;著名书法家项国就撰写的书法学术论文《探究〈散氏盘〉与其临创感悟》入编国家一级大型文化类期刊《中国民族博览》&#xff0c;并于 2023 年 1 月正式出刊发表。 据悉&#xff0c;《散氏盘》是我国最重要的书法艺术形式之一&#xff0c;研究《散氏盘…

并发编程之AtomicUnsafe

目录 原子操作 定义 术语 处理器如何实现原子操作 处理器自动保证基本内存操作的原子性 使用总线锁保证原子性 使用缓存锁保证原子性 Java当中如何实现原子操作 Atomic 定义 原子更新基本类型类 原子更新数组类 原子更新引用类型 原子更新字段类 Unsafe应用解析…

【MySQL】MySQL的锁机制

目录 概述 MyISAM 表锁 InnoDB行锁 概述 锁是计算机协调多个进程或线程并发访问某一资源的机制&#xff08;避免争抢&#xff09;。 在数据库中&#xff0c;除传统的 计算资源&#xff08;如 CPU、RAM、I/O 等&#xff09;的争用以外&#xff0c;数据也是一种供许多用户共…

中断以及 PIC可编程中断控制器

1 中断分为同步中断&#xff08;中断&#xff09;和异步中断&#xff08;异常&#xff09; 1.1 中断和异常的不同 中断由IO设备和定时器产生&#xff0c;用户的一次按键会引起中断。异步。 异常一般由程序错误产生或者由内核必须处理的异常条件产生。同步。缺页异常&#xff…

【Linux】关机、重启和用户登录注销

目录1 关机&重启命令1.1 基本介绍1.2 注意细节2 用户登录和注销2.1 基本介绍2.2 使用细节1 关机&重启命令 1.1 基本介绍 shutdown -h now          立刻关机shutdown -h 1            “hello&#xff0c;1分钟后会关机了”shutdown -r now   …

【hadoop】介绍

目录 介绍 版本 优势 大数据技术生态体系 介绍 Hadoop是一个由Apache基金会所开发的分布式系统基础架构。 解决 存储和分析计算Google在大数据方面的三篇论文GFS --->HDFS Map-Reduce --->MR BigTable --->HBaseHadoop创始人Doug Cutting版本 Hadoop 三大发行版本&a…

一文带你了解MySQL的Explain

日常工作中&#xff0c;我们经常会收到慢sql告警&#xff0c;前面也写过两篇优化文章&#xff0c;SQL的优化思路和使用规范_出世&入世的博客-CSDN博客 MySQL优化策略_出世&入世的博客-CSDN博客 在优化这些慢sql时&#xff0c;我们经常需要用到explain这个命令来查看…

aws dynamodb java低等级api和高级客户端api的使用

参考资料 https://docs.amazonaws.cn/zh_cn/sdk-for-java/latest/developer-guide/setup-project-maven.html 初始化环境 创建maven项目 mvn org.apache.maven.plugins:maven-archetype-plugin:3.1.2:generate \-DarchetypeArtifactId"maven-archetype-quickstart&quo…

正则表达式引擎NFA自动机的回溯解决方案总结

前几天线上一个项目监控信息突然报告异常&#xff0c;上到机器上后查看相关资源的使用情况&#xff0c;发现 CPU 利用率将近 100%。通过 Java 自带的线程 Dump 工具&#xff0c;我们导出了出问题的堆栈信息。 我们可以看到所有的堆栈都指向了一个名为 validateUrl 的方法&#…

【C语言】每日刷题 —— 牛客语法篇(4)

&#x1f680;&#x1f680;前言 大家好&#xff0c;继续更新专栏 c_牛客&#xff0c;不出意外的话每天更新十道题&#xff0c;难度也是从易到难&#xff0c;自己复习的同时也希望能帮助到大家&#xff0c;题目答案会根据我所学到的知识提供最优解。 &#x1f3e1;个人主页&am…

Mr. Cappuccino的第48杯咖啡——冒泡APP(升级版)之基于Docker部署企业级Maven私服

冒泡APP&#xff08;升级版&#xff09;之基于Docker部署企业级Maven私服基于Docker安装Nexus登录Maven私服配置Maven仓库配置settings.xml和pom.xml项目实践及常见问题基于Docker安装Nexus 查看sonatype/nexus镜像版本 下载指定版本的镜像 docker pull sonatype/nexus3:3.48…