C++中List的实现

news2024/11/17 23:32:42

前言

数据结构中,我们了解到了链表,但是我们使用时需要自己去实现链表才能用,但是C++出现了list将这一切皆变为现。list可以看作是一个带头双向循环的链表结构,并且可以在任意的正确范围内进行增删查改数据的容器。list容器一样也是不支持下标访问的,主要也是物理空间并不连续。

火车侧面图 的图像结果

list的部分重要接口实现

一样的,在实现之前,我们要对list的结构有一个大概的框架

 在此基础就很容易理解list的内部是一个个节点,该节点的类型也都是自定义类型,而模版参数是节点中val的类型。代码框架如下:

namespace cr
{
	template<class T>
	struct ListNode
	{
		ListNode(const T& x = T())//T()匿名对象
			:val(x)
			,pre(nullptr)
			,next(nullptr)
		{}
		T val;
		struct ListNode<T>* pre;
		struct ListNode<T>* next;
	};

	template<class T>
	class list
	{
		typedef ListNode<T> Node;//对类进行重命名

	public:
		
	private:
		Node* _head;//自定义类型成员的指针

	};

}

1.迭代器实现

cr::list<int>::iterator it = lt.begin();
while (it != lt.end())//lt.end()临时对象
{
	cout << *it << endl;
	it++;
}

先看这段代码,该代码也就是我们想要最终实现的结果,所以我们想知道对迭代器进行哪些操作,可以先从需要实现的功能进行分析。可以初步知道迭代器“八成”是节点的指针(节点是自定义类型),所以*it和it++这两个操作铁定是需要进行运算符重载的,*it实际就是得到节点类的成员变量val的值,而it++就是通过指针的偏移,指向下一个节点。而最重要的是应该知道*it和it++操作对象是迭代器而不是list实例化出来的对象(lt),像begin和end函数面向的就是list实例化的对象,所以可以直接在list类中实现。如果*it和it++也在在list类中实现的话,默认传参传的就是list实例化对象的地址,最后默认由this指针接受。而我们需要的是对迭代器进行操作,固然需要this指向迭代器的指针。所以应该将迭代器封装起来到一个类里里更理想的状态。


所以迭代器封装成类的话,那么肯定需要的成员对象就是节点指针,而成员函数就一定有operator*和operator++。其实operator!=是最容易忽略的,仔细想想迭代器类也是一个自定义类型,这么可以直接去判断呢,所以operator!=也是要实现的,所以下边整理了需要实现的运算符重载函数以及其他的一些。

代码如下:

template<class T>
struct _List_iterator
{
	typedef ListNode<T> Node;

	_List_iterator(Node* node)
		:_pnode(node)
	{}

	T& operator*()
	{
		return _pnode->val;
	}

	_List_iterator<T> operator++()//前置++
	{
		_pnode = _pnode->next;
		return *this;
	}

	_List_iterator<T> operator++(int)//后置++
	{
		_List_iterator tmp = *this;
		_pnode = _pnode->next;
		return tmp;
	}

	_List_iterator<T> operator--()
	{
		_pnode = _pnode->pre;
		return *this;
	}

	_List_iterator<T> operator--(int)
	{
		_List_iterator tmp = *this;
		_pnode = _pnode->pre;
		return tmp;
	}

	bool operator!=(const _List_iterator<T>& cmpnode)const 
	{
		return _pnode != cmpnode._pnode;
	}

    T* operator->()
	{
		return &_pnode->val;//一般用于存自定义类型对象时,会隐藏一个箭头
	}

	Node* _pnode;
};


这里operator->的实现你可能会有疑问,举例下列代码:

class A
{
public:
	A(int a=0,int b=0)
		:_a(a)
		,_b(b)
	{}
	int _a;
	int _b;
};
int main()
{
	cr::list<A> a;
	a.push_back(A(1, 1));//创建匿名对象初始化
	a.push_back(A(2, 2));
	a.push_back(A(3, 3));

	return 0;
}

面对这种情况,list实例化的是一个类的对象的话,而且想要打印出该类中成员变量的话,就是下面的方法:

cr::list<A>::iterator it = a.begin();
while (it != a.end())
{
	cout << (*it)._a << (*it)._b << endl;
	it++;
}

这里的(*it)就是list中的pnode->val也就是这个A类,但是并没有重载流运算符,所以就只能去访问里面的数据(公有),所以就是通过(.)运算符,既然可以用(.)那么肯定也可以用(->)所以就重载了(->)运算符,而(->)有点不一样:

T* operator->()
{
	return &_pnode->val;
}

返回值的类型的是A*一个类型,也就是返回一个指针,所以在调用(->)运算赋不得加上两个(->)了吗,一个是调用运算符重载函数返回A*,另一个是指针访问符,所以为了增强可读性就省略一个(->)

cr::list<A>::iterator it = a.begin();
while (it != a.end())
{
	cout << it->_a << it->_b << endl;
	it++;
}

以上两种写法实现结果都是一样的:



而对于begin和end函数面向的就是list实例化的对象,那么说就是在list类中实现就行

iterator begin()
{	
	//return iterator(_head->next);//匿名对象
	return _head->next;
}
iterator end()
{
	//return iterator(_head);//匿名对象
	return _head;
}

因为迭代器封装的类是单参数,所以可以直接返回节点的指针(会自动调用迭代器的构造函数)

 2.const迭代的实现

首先我们要了解list中const迭代器与非const迭代器的区别,const迭代器中的成员变量指向的数据是一个节点的指针,并不是说const修饰了以后这个节点不能改变,实际上是节点中的val成员变量不能改变。假如说是节点不能改变的话,那么就连pre和next这两个成员变量也不可以改变,如果连这两个都不能改变的话,那么还怎么实现operator++,还怎么遍历链表中的数据呢。

所以凭这一点,const迭代器就不可能是直接在iterator前面加上const限定符修饰那么容易。

而想要val的值不能改变的话,实际上就是operator*和operator->这两个函数的返回值要加const引用来修饰


方法一:再写一个_List_const_iterator的类

对于_List_terator类而言_List_const_iterator这个类只需要改一下类型而已,像operator*和operator->的返回值加上const修饰而其他需要返回迭代器的地方改成_List_const_iterator<T>就行,最后在list类中typedef  _List_const_iterator<T>  const_iterator就完成了


方法二:通过模版实现

方法一是需要再实现一个_List_const_iterator的类,但是本质上其实这个类相较于_List_iterator没什么区别,也就是返回值的类型有区别而已,所以此时对迭代器模版的引入就在合适不过了。而此时的模版参数肯定不能只有一个参数,而应该有三个参数:

template<class T,class ref,class ptr>

模版中第二个参数的实参其实就是const T&用于operator*运算符函数的返回值,而第三个参数的实参其实就是const T* 用于operator->函数的返回值。此时你可能会问为什么是需要这两个参数呢?其实判断方法很简单,就是通过实质上的比较const迭代器和非const迭代器实现时各函数返回值有哪些发生了改变。表面上你是只写了一个迭代器的类加一个模版就实现了两个迭代器干的活,实际上在编译的时候编译器通过模版参数自动生成了两个类,在使用时会优先找最匹配的调用。

所以这里提一点:模版中类名相同模版参数不同并不代表是同一个类型,类型=类名+模版参数

迭代器实现代码

template<class T,class ref,class ptr>
	struct _List_iterator
	{
		typedef ListNode<T> Node;

		_List_iterator(Node* node)
			:_pnode(node)
		{}

		ref operator*()
		{
			return _pnode->val;
		}
		ptr operator->()
		{
			return &_pnode->val;
		}

		_List_iterator<T,ref,ptr> operator++()
		{
			_pnode = _pnode->next;
			return *this;
		}
		_List_iterator<T,ref,ptr> operator++(int)
		{
			_List_iterator tmp = *this;
			_pnode = _pnode->next;
			return tmp;
		}
		_List_iterator<T,ref,ptr> operator--()
		{
			_pnode = _pnode->pre;
			return *this;
		}
		_List_iterator<T,ref,ptr> operator--(int)
		{
			_List_iterator tmp = *this;
			_pnode = _pnode->pre;
			return tmp;
		}
		bool operator!=(const _List_iterator<T,ref,ptr>& cmpnode)const
		{
			return _pnode != cmpnode._pnode;
		}

		Node* _pnode;
	};

其实上面的代码本质上就是将operator*和operator->的返回值虚拟化,然后在调用迭代器时才进行实例化的。 

	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;

		const_iterator begin()const
		{  
			//return iterator(_head->next);//匿名对象
			return _head->next;
		}
		const_iterator end()const
		{
			//return iterator(_head);//匿名对象
			return _head;
		}

        ......
    }

在list类中分别将两种类型迭代器重命名,所以在外面调用哪种迭代器时就会在内部模版实例化成具体的迭代器

3.list的插入与删除

        iterator insert(iterator pos,const T& x)
		{
			Node* l1 = pos._pnode->pre;
			Node* l2 = pos._pnode;
			Node* tmp = new Node(x);

			l1->next = tmp;
			tmp->pre = l1;
			tmp->next = l2;
			l2->pre = tmp;

			return tmp;
		}
		iterator erase(iterator pos)//调用之后pos节点失效
		{
			Node* l1 = pos._pnode->pre;
			Node* l2 = pos._pnode->next;

			l1->next = l2;
			l2->pre = l1;
			delete pos._pnode;

			return l2;
		}

        void push_back(const T& x)
		{
			insert(this->end(), x);
			//Node* tmp = new Node(x);
			//Node* tail = _head->pre;//找尾

			//tail->next = tmp;
			//tmp->pre = tail;
			//tmp->next = _head;
			//_head->pre = tmp;
		}

这里的插入删除同链表一样,没什么好说的,唯一要注意的就是迭代器失效的问题,list的insert不存在扩容换址,所以不会造成迭代器失效,但是list的erase就不同了,earase会释放当前的节点,所以调用之后就会失效。所以给erase一个返回值,返回需要删除的节点的下一个节点。

4.list的构造加赋值

        list()
			:_head(nullptr)
		{
			_head = new Node;//该节点也是一个类,创建时调用构造函数
			_head->next = _head;
			_head->pre = _head;
		}
		list(const list<T>& copy)
			:_head(nullptr)
		{
			_head = new Node;//该节点也是一个类,创建时调用构造函数
			_head->next = _head;
			_head->pre = _head;

			for (auto it : copy)//内部实际是用迭代器
			{
				push_back(it);
			}
		}
        list<T>& operator=(list<T> copy)//调用拷贝构造
		{
			std::swap(_head, copy._head);
			
			return *this;
		}

因为list容器类似带头双向循环链表,所以构造时要new一个带头节点,其次要注意的是new的这个节点的类型是我们自定义的类型,所以在new时就会自动调用该节点的构造函数将其空间内容初始化。赋值函数就实现的比较巧妙,通过传参调用拷贝构造函数的深拷贝,然后再换值就行。

5.空间释放析构函数

        void clear()
		{
			list::iterator del = this->begin();
			while (del != this->end())
			{
				del = erase(del);
			}
			_size = 0;
		}
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

若有不恰当的描述欢迎留言指正!

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

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

相关文章

每天一道leetcode:1926. 迷宫中离入口最近的出口(图论中等广度优先遍历)

今日份题目&#xff1a; 给你一个 m x n 的迷宫矩阵 maze &#xff08;下标从 0 开始&#xff09;&#xff0c;矩阵中有空格子&#xff08;用 . 表示&#xff09;和墙&#xff08;用 表示&#xff09;。同时给你迷宫的入口 entrance &#xff0c;用 entrance [entrancerow, …

7.原 型

7.1原型 【例如】 另外- this指向&#xff1a; 构造函数和原型对象中的this都指向实例化的对象 7.2 constructor属性 每个原型对象里面都有个constructor属性( constructor构造函数) 作用&#xff1a;该属性指向该原型对象的构造函数 使用场景: 如果有多个对象的方法&#…

如何使用Kali Linux进行密码破解?

今天我们探讨Kali Linux的应用&#xff0c;重点是如何使用它来进行密码破解。密码破解是渗透测试中常见的任务&#xff0c;Kali Linux为我们提供了强大的工具来帮助完成这项任务。 1. 密码破解简介 密码破解是一种渗透测试活动&#xff0c;旨在通过不同的方法和工具来破解密码…

ubuntu 编译安装nginx及安装nginx_upstream_check_module模块

一、下载安装包 # 下载nginx_upstream_check_module模块 wget https://codeload.github.com/yaoweibin/nginx_upstream_check_module/zip/master# 解压 unzip master# 下载nginx 1.21.6 wget https://github.com/nginx/nginx/archive/refs/tags/release-1.21.6.tar.gz # 解压…

【Spring 】了解Spring AOP

目录 一、什么是Spring AOP 二、AOP的使用场景 三、AOP组成 四、Spring AOP的实现 1、添加Spring AOP依赖 2、定义切面和切点 3、定义相关通知 五、 AOP的实现原理 1、什么是动态代理 2、 JDK代理和CGLIB代理的区别 一、什么是Spring AOP AOP&#xff08;Aspect Ori…

机器学习|Softmax 回归的数学理解及代码解析

机器学习&#xff5c;Softmax 回归的数学理解及代码解析 Softmax 回归是一种常用的多类别分类算法&#xff0c;适用于将输入向量映射到多个类别的概率分布。在本文中&#xff0c;我们将深入探讨 Softmax 回归的数学原理&#xff0c;并提供 Python 示例代码帮助读者更好地理解和…

Java之SpringCloud Alibaba【四】【微服务 Sentinel服务熔断】

Java之SpringCloud Alibaba【四】【微服务 Sentinel服务熔断】 一、分布式系统遇到的问题1、服务挂掉的一些原因 二、解决方案三、Sentinel&#xff1a;分布式系统的流量防卫兵1、Sentinel是什么2、Sentinel和Hystrix对比3、Sentinel快速开发4、通过注解的方式来控流5、启动Sen…

【Android Framework系列】第11章 LayoutInflater源码分析

1 前言 本章节我们主要目目的是了解Activity的xml布局解析、对LayoutInfater源码进行分析。 我们知道Android界面上的每一个控件都是一个个View&#xff0c;但是Android也提供了通过xml文件来进行布局控制&#xff0c;那么xml布局文件如何转成最终的View的呢&#xff1f;转换利…

java面试基础 -- ArrayList 和 LinkedList有什么区别

目录 基本介绍 有什么不同?? ArrayList的扩容机制 ArrayLIst的基本使用 基本介绍 还记得我们的java集合框架吗, 我们来复习一下, 如图: 可以看出来 ArrayList和LinkedList 都是具体类, 他们都是接口List的实现类. 但是他们底层的逻辑是不同的, 相信学过这个的应该大概有…

idea下plantuml报错 dot executeable /opt/local/bin/dot

https://blog.csdn.net/weixin_40509040/article/details/121222419 注意路径中的版本问题&#xff0c;以及在完成这些操作后&#xff0c;重启软件&#xff0c;就可以正常了

SpringBoot集成Solr(一)保存数据到Solr

SpringBoot集成Solr&#xff08;一&#xff09;保存数据到Solr 添加依赖 <!--SpringBoot中封装过的Solr依赖--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-solr</artifactId><ver…

【Linux】进程信号篇:信号的产生(signal、kill、raise、abort、alarm)、信号的保存(core dump)

文章目录 一、 signal 函数&#xff1a;用户自定义捕捉信号二、信号的产生1. 通过中断按键产生信号2. 调用系统函数向进程发信号2.1 kill 函数&#xff1a;给任意进程发送任意信号2.2 raise 函数&#xff1a;给调用进程发送任意信号2.3 abort 函数&#xff1a;给调用进程发送 6…

Rest学习环境搭建:服务提供者

创建一个普通的Maven项目 pom.xml父工程 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http…

科大讯飞发布星火认知大模型2.0版——体验实测

8月15日&#xff0c;科大讯飞举行讯飞星火认知大模型V2.0升级发布会&#xff0c;对外展示其升级后的大模型代码能力和多模态能力&#xff0c;同时发布并升级搭载讯飞星火认知大模型V2.0能力的多项应用和产品。自5月6日首发以来&#xff0c;星火认知大模型经历V1.5版本的迭代&am…

面试之快速学习STL-deuqe和list

1. deque deque 容器用数组&#xff08;数组名假设为 map&#xff09;存储着各个连续空间的首地址。也就是说&#xff0c;map 数组中存储的都是指针如果 map 数组满了怎么办&#xff1f;很简单&#xff0c;再申请一块更大的连续空间供 map 数组使用&#xff0c;将原有数据&…

利用Figlet工具创建酷炫Linux Centos8服务器-登录欢迎界面-SHELL自动化编译安装代码

因为我们需要生成需要的特定字符,所以需要在当前服务器中安装Figlet,默认没有安装包的,其实如果我们也只要在一台环境中安装,然后需要什么字符只要复制到需要的服务器中,并不需要所有都安装。同样的,我们也可以利用此生成的字符用到脚本运行的开始起头部分,用ECHO分行标…

go_并发编程(1)

go并发编程 一、 并发介绍1&#xff0c;进程和线程2&#xff0c;并发和并行3&#xff0c;协程和线程4&#xff0c;goroutine 二、 Goroutine1&#xff0c;使用goroutine1&#xff09;启动单个goroutine2&#xff09;启动多个goroutine 2&#xff0c;goroutine与线程3&#xff0…

log4j:WARN No appenders could be found for logger问题

本文将idea场景下的使用。 IDEA中&#xff0c;将配置文件命名为log4j.properties&#xff08;该命名才会被自动加载&#xff09;&#xff0c; 并放到某个目录下&#xff08;通常放到resources目录&#xff09;&#xff0c;并在resources上右键&#xff0c;找到Mark Directory a…

(二)结构型模式:8、代理模式(Proxy Pattern)(C++示例)

目录 1、代理模式&#xff08;Proxy Pattern&#xff09;含义 2、代理模式的UML图学习 3、代理模式的应用场景 4、代理模式的优缺点 5、C实现代理模式的实例 1、代理模式&#xff08;Proxy Pattern&#xff09;含义 代理模式&#xff08;Proxy&#xff09;&#xff0c;为…

【LangChain】P1 LangChain 应用程序的核心构建模块 LLMChain 以及其三大部分

LangChain 的核心构建模块 LLMChain LangChain 应用程序的核心构建模块语言模型 - LLMs提示模板 - Prompt templates输出解析器 - Output Parsers LLMChain 组合 LangChain 应用程序的核心构建模块 LangChain 应用程序的核心构建模块 LLMChain 由三部分组成&#xff1a; 语言…