首页 > 编程语言 >C++容器list底层迭代器的实现逻辑~list相关函数模拟实现

C++容器list底层迭代器的实现逻辑~list相关函数模拟实现

时间:2024-09-22 14:50:06浏览次数:8  
标签:node 这个 迭代 list C++ 我们 prev 节点

目录

1.两个基本的结构体搭建

2.实现push_back函数

3.关于list现状的分析(对于我们如何实现这个迭代器很重要)

3.1和string,vector的比较

3.2对于list的分析

3.3总结

4.迭代器类的封装

5.list容器里面其他函数的实现

6.个人总结

7.代码附录


1.两个基本的结构体搭建

首先就是我们的这个list结构体,这个里面包含了我们的基本的函数的实现,以便于我们后期的测试;

这个里面的成员变量就是现在的这个_head即我们的哨兵位节点,这个节点是我们链表的第一个节点,但是是没有实际含义的,他的下一个节点才是我们的链表里面的真正的第一个节点;

因为我们的这个list表示的就是一个双向的链表,因此这个里面的每一个元素都是我们的节点,我们的节点里面又包含了数据域和指针域,因此这个需要我们重新定义一个node的结构体,方便我们在这个list里面进行使用;

其中这个里面的list_node结构体里面包含的内容就是我们的data元素和next指针变量;


  • 这个下面的list_node就是一个模版类,list_node<T>表示的就是模版类的类型,其中这个里面的T可以是任意的数据类型,例如这个T就是我们熟悉的int类型,这个时候的,list_node<int>表示的就是这个类里面的数据就是int类型的数据,这个前驱指针和后继指针就是int*类型的;
  • _naxt表示的就是后继指针,_prev就是前驱指针,都是英文的缩写;


  • 这个里面为了简答起见,我们对于这个list_node进行重定义为Node,这样我们后面使用的时候就会方便一些;
  • 这个里面是对于 list链表进行初始化的工作,list()就是一个构造函数,首先我们使用new开辟新的节点,这个时候这个节点就是我们的链表里面的唯一的节点;
  • 所以这个_haed头结点的前驱指针和后继指针指向的都是自己;

2.实现push_back函数

为什么首先实现这个push_back函数,这个函数实现的就是向这个链表里面插入数据,我们想要使用迭代器进行遍历首先这个链表里面需要有数据,因此我们使用这个push_back函数向这个链表里面添加数据;

代码解读

  • 首先使用new开辟一个新的节点(第一行代码),让原来的tai的next指针l指向这个新的节点(第三行代码);
  • 我们的tail这个时候就是_haed这个头结点的前驱指针(第二行代码),因为我们的这个链表是一个双向的链表,第一个头结点和最后一个尾结点之间有联系;
  • newnode的前后建立联系,前面是我们的这个原来的tail节点,后面的后继指针指向的就是我们的头结点(第4,5行代码);
  • 最后一行表示的是这个_head头结点的前驱节点就是我们的这个新开辟的newnode节点;


此外,我们可以实现一些基本的函数供我们使用,这个包括了size函数计算这个链表里面的节点的个数,以及使用这个_size成员变量进行判断我们的这个链表是不是空的;

3.关于list现状的分析(对于我们如何实现这个迭代器很重要)

3.1和string,vector的比较

我们的迭代器就是对于这个容器里面的元素进行遍历的,我们的之前介绍的string和vector都是支持这个迭代器的,这个list也支持,但是没有那么随意;

什么是随意,就是我们的这个string vector因为自身的这个连续性,因为string就是相当于我们学过的这个字符串,vector就是类似于我们学习的这个数组,他们的这个空间都是连续的,我们可以使用这个++,--运算符对于这个迭代器的指针进行移动,进而对于这个容器里面的元素进行遍历;

而且我们对上面的两个string,vector使用这个迭代器解引用就可以直接拿到这个对应位置的数据,这些都是我们list链表无法直接做到的;

3.2对于list的分析

因为我们的list里面的节点通过指针进行连接,这个里面的指针分为前驱指针和后继指针,其中这个指针里面存放了下一个元素的地址;

这个时候我们无法使用++,--直接拿到相邻位置的元素,因为我们的++,--只是拿到的物理上连续空间的地址,但是这个list节点之间的物理地址不是连续的;

但是我们可以通过一定的手段去拿到,因为我们知道下一个元素的地址;

同理,我们通过解引用也没有办法得到这个位置的数据,因为我们拿到的是节点,这个里面有数据域和指针域,而我们想要实现的效果就是通过解引用直接得到这个数据;

但是我们可以通过一定的方法,例如使用这个运算符的重载,把这个解引用操作符重载成为直接拿到这个节点对应的数据的操作符;

3.3总结

因此,我们需要封装一个类,实现这个operator*和operator++的重载,进而可以让我们达到我们想要的效果;

4.迭代器类的封装

这个里面封装了我们的operator*和operator++两个运算符,都是我们经过上面的这个对于list容器的现状的分析之后得到的结论:我们应该使用这个*运算符的重载直接得到这个节点对应位置的数据,使用这个++运算符直接找到这个链表里面的下一个节点;

当我们在进行遍历的时候,我们的两个迭代器不相等就需要接着进行遍历,当相等的时候就需要停止这个便利的过程,因此我们还重载了这个里面的operator!=运算符方便我们对于这个遍历的过程进行控制;

其中在这个++运算符的重载里面,我们就是直接把下一个节点的指针赋值给当前的这个节点,返回值就是赋值之后的这个新的节点,这样就实现了这个++运算符的重载;

我们的list里面也是对于这个list_iterator进行重定义,这个名字和上面的这个self的意义是一样的,就是这个表达上不相同罢了,因为我们实际上进行遍历还是使用的这个iterator迭代器,这个重命名就是为了我们使用方便;

我们的这个begin函数返回值是一个迭代器,但是我们return的就是一个指针,但是我们的list_iterator里面是一个单参数的构造函数,因此这个是可以支持隐式类型转换的;

完整代码:写到这个地方,我们基本的这个逻辑就已经实现了,这个时候就可以使用这个迭代器对于这个list容器里面的元素进行遍历了;

//#define _CRT_SECURE_NO_WARNINGS 1
//list.h文件

#pragma once
#include<iostream>
using namespace std;

namespace bite
{
	template<class T>

	//对于节点定义一个类
	struct list_node
	{
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		list_node(const T& data = T())
			:_data(data)
			, _prev(nullptr)
			, _next(nullptr)
		{

		}
	};
	template<class T>
	
	struct list_iterator
	{
		typedef list_node<T> Node;

		typedef list_iterator<T>  self;
		Node* _node;

		list_iterator(Node* node)
			:_node(node)
		{

		}

		//使用引用是因为我们可以修改这个里面的数据
		T& operator*()
		{
			return _node->_data;
		}

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

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


	template<class T>

	class list
	{
		typedef list_node<T> Node;
	public:
		typedef list_iterator<T>  iterator;

		//返回的是一个节点,接受的是迭代器,但是这个是单参数构造函数支持隐式类型转换
		iterator begin()
		{
			//iterator it(_head->next);

			return _head->_next;
		}

		iterator end()
		{
			//最后一个元素的下一个位置
			return _head;
		}
		list()
		{
			_head = new Node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}

		void push_back(const T& x)
		{
			Node* newnode = new Node(x);
			Node* tail = _head->_prev;

			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

		size_t size() const
		{
			return _size;
		}

		bool empty() const
		{
			return _size == 0;
		}
	private:
		Node* _head;
		size_t _size;
	};

	void test_list1()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		list<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

}
#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
//#include<algorithm>
//#include<list>
//#include<vector>
//using namespace std;

#include"list.h"

int main()
{
	bite::test_list1();
	return 0;
}

 这个时候,我们对于这个迭代器进行测试,发现这个迭代器就可以正常的跑起来了;

5.list容器里面其他函数的实现

首先就是这个insert和erase,即链表里面的数据的插入和删除;

插入数据的话,就是新建一个节点,在这个节点的前后和这个节点建立联系,因为是双向的,就是指针之间的相互指向的确定(4行代码);

erase就是把这个节点的前面和后面的节点确定,让前后两个节点相互指向,然后把这个节点释放掉,这个节点就被删除掉了;

实现了上面的这个insert和erase之后,我们想要在这个指定的位置进行数据的插入和删除就比较容易了,我们只需要调用上面的函数即可;

push_back不需要之前写的那么复杂,直接在这个end()前面插入数据即可;

push_front就是在这个begin()前面插入数据(这个里面的begin指向的是第一个有实际意义的节点,不是我们的哨兵位头结点,这个一定要注意!!!!!!!!!!!!!)

pop_front就是删除这个头结点的数据,直接调用这个erase把这个begin()传递进去即可;

pop_back函数就是删除这个最后的数据,因为我们的end指向的是最后一个数据的下一个位置,因此这个地方我们进行传参的时候进行了--操作;

6.个人总结

在实现这个迭代器的时候,我们需要搞清楚这个架构,实际上就是三个结构体,一个是节点的,一个是链表的,一个就是我们自己封装的这个迭代器,其中这个节点的结构体就是为了方便使用;

其次这个不断地进行typedef对于初学者也是一个挑战,不是因为它很难,而是在这个重命名之后,我们需要这个结构重命名之前是什么,这个结构的里面有哪些内容,例如pos._node我就理解了很久,就是因为重命名之后对于这个结构里面的内容不清楚导致的;

实际上,这个pos就是我们的参数iterator,本质上就是迭代器,这个list_iterator结构体里面就有我们的这个_node成员,因此这个pos._node就不难理解了,实际上这个struct使用就是因为我们的struct默认的就是公有的,符合我们的使用要求,使用class也是可以的,但是要加上这个public表示我们的权限,因此class里面的内容默认是私有的,使用class之后,我们这个代码风格里面的pos._node就应该相应的修改;

另外,我们的这个迭代器使用的时候看似和其他的这个string .vector没有区别,就是进行遍历,但是实际上我们是封装了一个类的,为了封装这个类,我们定义了其他的两个类,这些都是我们在调用时候看不到的,我们只看到了这个迭代器可以进行遍历,但是实际上这个背后的功夫确是我们无法估量的,如果你认为很简单,自己独立实现一下就知道这个过程的难度了;

这个就好比我们普通的孩子和生来就条件好的孩子,生来条件就好的孩子好比string,vector人家就是可以直接调用这个*找到这个位置的数值,使用这个++进行这个循环的控制,但是我们普通人家的孩子就是list,我们没有他们的优势,但是我们可以通过自己的努力,实现一个类的封装,我们也可以实现相同的效果,就看肯不肯去克服实现这个类的路上的困难了;

看似就是一个list,却让我们从中看到了大部分人的影子,因为我们大部分都是list,没有先天的优势,但是只要我们肯付出努力,就可以实现相同的遍历的效果,因此,努力吧少年~我命由我不由天,努力奋进改尘寰~~我们要相信自己的努力,就是这个迭代器封装的类,我们最后也是会实现相同的效果的,list就是证明~~

7.代码附录

//#define _CRT_SECURE_NO_WARNINGS 1
//list.h文件,包含迭代器和一些常用的函数

#pragma once
#include<iostream>
using namespace std;

namespace bite
{
	template<class T>

	//对于节点定义一个类
	struct list_node
	{
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		//内置类型:0,0.0,空指针
		//自定义类型:调用默认构造,调用自己的模版
		list_node(const T& data = T())//第一次修改,-------给默认构造
			:_data(data)
			, _prev(nullptr)
			, _next(nullptr)
		{

		}
	};
	template<class T>
	
	struct list_iterator
	{
		typedef list_node<T> Node;

		typedef list_iterator<T>  self;
		Node* _node;

		list_iterator(Node* node)
			:_node(node)
		{

		}

		//使用引用是因为我们可以修改这个里面的数据
		T& operator*()
		{
			return _node->_data;
		}

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

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

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

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


	template<class T>

	class list
	{
		typedef list_node<T> Node;
	public:
		typedef list_iterator<T>  iterator;

		//返回的是一个节点,接受的是迭代器,但是这个是单参数构造函数支持隐式类型转换
		iterator begin()
		{
			//iterator it(_head->next);

			return _head->_next;
		}

		iterator end()
		{
			//最后一个元素的下一个位置
			return _head;
		}
		list()
		{
			_head = new Node(T());//这个是第一次报错的原因,---------------修改的地方
			//第一次报错是因为没有写这个地方的node的构造函数
			//new Node(x);需要我们传递匿名对象
			_head->_next = _head;
			_head->_prev = _head;
		}

		void push_back(const T& x)
		{
			/*Node* newnode = new Node(x);
			Node* tail = _head->_prev;

			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;*/
			insert(end(), x);
		}

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

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

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

		void insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;

			Node* newnode = new Node(x);

			// prev newnode cur
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
			prev->_next = newnode;

			++_size;
		}

		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;

			--_size;
		}

		size_t size() const
		{
			return _size;
		}

		bool empty() const
		{
			return _size == 0;
		}
	private:
		Node* _head;
		size_t _size;
	};

	void test_list1()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		list<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

}
#define _CRT_SECURE_NO_WARNINGS 1
//test.cpp测试文件,验证我们的迭代器的功能,是否可以正常的遍历链表
#include<iostream>
//#include<algorithm>
//#include<list>
//#include<vector>
//using namespace std;

#include"list.h"

int main()
{
	bite::test_list1();
	return 0;
}

标签:node,这个,迭代,list,C++,我们,prev,节点
From: https://blog.csdn.net/binhyun/article/details/142434637

相关文章

  • 【蓝桥杯】2024.9.22算法赛——灵魂问题\全栈项目小组(C++)
    一、灵魂问题题目灵魂问题题目分析1.要求输出一个整数2.题目在玩脑筋急转弯,关键句子标出来了——糖什么的根本不重要。所以咖啡不加糖,答案是0!!!代码#include<iostream>usingnamespacestd;intmain(){ cout<<0; return0;}二、全栈项目小组题目全栈项目小组......
  • C++ 线程池
    #include<iostream>#include<string>#include<memory>#include<vector>#include<thread>#include<queue>#include<functional>#include<mutex>usingnamespacestd;classThreadPool{public:Thread......
  • C++:数组与字符串
    一、数组         数组是一种存储若干元素的数据类型,在诸多编程语言中存在,其显著的特点是元素通常是在物理层面上连续存储的(逻辑上的数组,比如链表,可能不是),并且具有极快的元素访问速度。    数组通常是同构的(homogenous ),即数组中的元素都是同一类型的,......
  • [数据结构与算法·C++] 笔记 1.4 算法复杂性分析
    1.4算法复杂性分析算法的渐进分析数据规模n逐步增大时,f(n)的增长趋势当n增大到一定值以后,计算公式中影响最大的就是n的幂次最高的项其他的常数项和低幂次项都可以忽略大O表示法函数f,g定义域为自然数,值域非负实数集定义:如果存在正数c和n,使得对任意的......
  • Filter 过滤器和 Listener 监听器
    2、开发步骤3、过滤器执行流程4、过滤器生命周期5、过滤器配置问题6、过滤器链(配置多个过滤器)二、Listener监听器1、ServletContextListener接口2、开发步骤一、Filter过滤器============1、概述当访问服务器资源时,Filter过滤器可以将请求拦截下来,完成一些特殊的......
  • C++ 异步 async future 等
    async和future这个和C#的Task有点像。#include<iostream>#include<string>#include<memory>#include<future>#include<thread>usingnamespacestd;intcalculate(){std::this_thread::sleep_for(std::chrono::seconds(2));......
  • 用友U8CRM relobjreportlist.php SQL注入漏洞复现
    0x01漏洞描述:        用友U8CRM客户关系管理系统是一款专业的企业级CRM软件,旨在帮助企业高效管理客户关系、提升销售业绩和提供优质的客户服务。        用友U8CRM客户关系管理系统relobjreportlist.php 文件存在SQL注入漏洞,未经身份验证的攻击者通过漏......
  • C++ std::call_once 实现单例模式
    #if1#include<iostream>#include<memory>#include<mutex>usingnamespacestd;classSingleton{public:staticSingleton&getInstance(){std::call_once(m_OnceFlag,&Singleton::init);return*m_Insta......
  • 【C/C++】速通涉及string类的经典编程题
    【C/C++】速通涉及string类的经典编程题一.字符串最后一个单词的长度代码实现:(含注释)二.验证回文串解法一:代码实现:(含注释)解法二:(推荐)1.函数isalnum介绍:2.函数tolower介绍:3.代码实现:三.翻转字符串II:区间部分翻转代码实现:(含注释)四.翻转字符串III:翻转字符串中的单词代......
  • 01背包问题之背包容量为什么要倒序遍历?(以C++代码具体实现为例)
    首先是先阐述一下背包问题:有n件物品和一个最多能背重量为w的背包。第i件物品的重量是weight[i],得到的价值是value[i]。每件物品只能用依次,求解将哪些物品装入背包里物品价值总和最大。这里不解释代码的其他部分,只对代码中的背包容量遍历进行具体的解释,首先给出遍历部分的代......