【C++】STL---list

news2024/11/18 23:49:16

STL---list

  • 一、list 的介绍
  • 二、list 的模拟实现
    • 1. list 节点类
    • 2. list 迭代器类
      • (1)前置++
      • (2)后置++
      • (3)前置- -、后置- -
      • (4)!= 和 == 运算符重载
      • (5)* 解引用重载 和 -> 重载
    • 3. list 类
      • (1)迭代器
      • (2)修改相关的接口
        • swap()
        • insert()
        • erase()
        • push_back、push_front、pop_back、pop_front
        • clear()
      • (3)空链表初始化
      • (4)构造函数
      • (5)拷贝构造函数
      • (6)赋值运算符重载
      • (7)析构函数
    • 4. 打印容器的接口
      • (1)打印链表整型的接口
      • (2)打印 list 的接口
      • (3)打印容器的接口

一、list 的介绍

  1. list 是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
  2. list 的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
  3. listforward_list 非常相似:最主要的不同在于 forward_list 是单链表,只能朝前迭代,已让其更简单高效。
  4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
  5. 与其他序列式容器相比,listforward_list 最大的缺陷是不支持任意位置的随机访问,比如:要访问 list 的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list 还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大 list 来说这可能是一个重要的因素)。

二、list 的模拟实现

list 学习时也要学会查看文档:list 文档介绍,在实际中我们熟悉常见的接口就可以,下面我们直接开始模拟实现,在模拟实现中我们实现的是常见的接口,并且会在实现中讲解它们的使用以及注意事项。

首先跟以往不一样的是,list 是一个个节点连接起来的,所以它不是连续的物理空间,这也就意味着,它不用扩容,每次插入的时候只需要申请一个节点,然后连接起来即可;

其次,list 底层的迭代器实现也跟 stringvector 不一样,它们两个的迭代器可以说是原生指针,但是 list 的迭代器是要让节点指向下一个节点,所以底层实现也不一样;例如我们想让迭代器 it,往后迭代,就是 ++it,但是底层的实现却不是真的让节点++,因为它们的空间不是连续的,所以我们要把 list 迭代器封装成一个类。

首先我们先创建一个自己的命名空间,把 list 节点的类,list 迭代器的类,list 类都放进去;

1. list 节点类

list 节点类如下,因为是双向链表,所以应该有一个数据,两个指针;

		namespace Young
		{
			// 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)
				{}
		
			};
		}

2. list 迭代器类

首先我们先定义一个类模板,其参数有三个,分别是类型类型的引用(const 和 非const)类型的指针(const 和 非const)

为什么要定义三个模板参数呢,因为考虑到 const 迭代器const 迭代器和普通迭代器不是同一个类,不能直接在 iterator 前直接加 const,如 const iterator ,这不是 const 迭代器,因为这里的 const 修饰的是迭代器本身,就是迭代器本身不能修改,但是我们期望的是迭代器本身可以被修改,如 it++、++it,只是期望迭代器指向的内容不能被修改,如 *it = 10、it->10

这就类比 const T*T* constconst T*const 是修饰指向的内容不能被修改,而 T* constconst 修饰的是指针本身不能被修改;而我们需要实现的 const 迭代器 是要满足第一种的,所以 list普通迭代器const 迭代器 是两个完全不一样的类,应该写成两个类,但是我们可以通过增加两个模板参数 类型的引用(const 和 非const)类型的指针(const 和 非const) 来复用普通迭代器,具体实现如下:

		// list 迭代器类
		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* node)
				:_node(node)
			{}
		}

首先我们先将节点类起别名为 Node,再将自己的类起别名为 self;迭代器本身也是一个指针,只是它内部实现不一样,所以我们需要一个 _node 节点的指针,构造函数实例化一个节点的指针,比如说 list<int>::iterator it = lt.begin();,这里的 it 就会调构造函数,实例化一个 lt.begin() 节点的指针,其实 lt.begin() 就是指向头节点的指针。

接着我们重载一些迭代器常用的运算符:

(1)前置++

就是让迭代器往后迭代,具体的实现就是让节点的指针指向下一个节点:

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

(2)后置++

跟前置++的区别就是,后置++需要拷贝,返回++以前的迭代器,所以一般都不用后置++;

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

(3)前置- -、后置- -

前置- -、后置- - 与 ++ 的区别就是, - -返回上一个节点的迭代器;

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

(4)!= 和 == 运算符重载

!= 运算符重载就是比较它们的节点是否相等;== 运算符就相反;

			// != 运算符重载   iterator it != lt.begin();
			bool operator!=(const self& s)
			{
				return s._node != _node;
			}
	

			// == 运算符重载   iterator it == lt.begin();
			bool operator==(const self& s) 
			{
				return s._node == _node;
			}

(5)* 解引用重载 和 -> 重载

解引用重载-> 重载 就是改变迭代器指向内容的两个运算符,所以我们定义的三个模板参数,就在这里起作用了;比如我们实例化的模板参数是 const 迭代器__list_iterator<T, const T&, const T*>,这里的 const T& 就是 Refconst T* 就是 Ptr,这里就可以直接用 Ref (解引用重载)和 Ptr(箭头重载) 作返回值;

如果是 非const 迭代器__list_iterator<T, T&, T*>T& 就是 RefT* 就是 Ptr;所以就可以根据它们的类型返回对应的迭代器类型,就不需要我们自己写两个迭代器的类了。

			// * 解引用重载
			Ref operator*()
			{
				return _node->_data;
			}
	
			// -> 重载
			Ptr operator->()
			{
				return &_node->_data;
			}

解引用-> 重载的使用:

假设 list 里面存的类型是一个自定义类型,这个自定义类型中有两个成员变量,那么我们在使用 解引用-> 重载的时候,应该访问哪一个呢?这时候就需要我们指定访问了,如下代码:

		struct AA
		{
			AA(int a1 = 0, int a2 = 0)
				:_a1(a1)
				, _a2(a2)
			{}
		
			int _a1;
			int _a2;
		};
		
		void test4()
		{
			Young::list<AA> lt;
			lt.push_back(AA(1, 1));
			lt.push_back(AA(2, 2));
			lt.push_back(AA(3, 3));
		
			Young::list<AA>::iterator it = lt.begin();
			while (it != lt.end())
			{
				// 使用解引用
				//cout << (*it)._a1<<" "<<(*it)._a2 << endl;
				
				//使用 ->
				cout << it->_a1 << " " << it->_a2 << endl;

				++it;
			}
			cout << endl;
		}

上面的 cout << it->_a1 << " " << it->_a2 << endl; 调用了->重载,实际上是 cout << it.operator->()->_a1 << " " << it.operator->()->_a2 << endl;,本来应该是有两个 -> 的,即 it->->_a1 但是这样写可读性不好,所以编译器特殊处理,省略了一个 ->

3. list 类

list 类首先将 const 迭代器和非 const 迭代器类型起别名为 const_iteratoriterator ;成员变量有 _head 哨兵位节点和 _size 记录链表的长度,如下:

		// list 类
		template <class T>
		class list
		{
		public:
			typedef list_node<T> Node;
			typedef __list_iterator<T, T&, T*> iterator;
			typedef __list_iterator<T, const T&, const T*> const_iterator;
			
		private:
			Node* _head;
			size_t _size;
		};

(1)迭代器

注意,begin() 是哨兵位的下一个节点,end() 是哨兵位节点。

begin()end() 返回的类型也是一个迭代器,这里 iterator(_head->_next) 是调用迭代器类的构造函数,构造一个节点的指针返回;也可以写成 _head->_next,因为支持隐式类型的转换;

			// 非 const 迭代器
			iterator begin()
			{
				return iterator(_head->_next);
			}
	
			iterator end()
			{
				return iterator(_head);
			}
			
			
			// const 迭代器
			const const_iterator begin() const
			{
				return const_iterator(_head->_next);
			}
	
			const const_iterator end() const
			{
				return const_iterator(_head);
			}

(2)修改相关的接口

swap()

交换链表数据,需要借助标准库的 swap 函数实现:

			// 交换链表数据
			void swap(list<T>& lt)
			{
				std::swap(_head, lt._head);
				std::swap(_size, lt._size);
			}

insert()

pos 迭代器插入节点;新开一个节点,然后插入指定迭代器的位置,连接好 prevcur 的位置即可;因为 list 的底层结构为带头结点的双向循环链表,因此在 list 中进行插入时是不会导致 list 的迭代器失效的;

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

erase()

删除 pos 迭代器位置的节点;在删除时迭代器会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响,所以 erase() 函数执行后,it 所指向的节点已被删除,因此 it 无效,在下一次使用 it 时,必须先给其赋值;

			// 删除节点
			iterator erase(iterator pos)
			{
				Node* prev = pos._node->_prev;
				Node* next = pos._node->_next;
	
				prev->_next = next;
				next->_prev = prev;
	
				delete pos._node;
				pos._node->_next = pos._node->_prev = nullptr;
	
				--_size;
	
				return next;
			}

push_back、push_front、pop_back、pop_front

只需要复用 insert()erase() 即可,实现如下:

			// 尾插
			void push_back(const T& x)
			{
				insert(end(), x);
			}
	
			// 头插
			void push_front(const T& x)
			{
				insert(begin(), x);
			}
	
			// 尾删
			void pop_back()
			{
				erase(--end());
			}
	
			// 头删
			void pop_front()
			{
				erase(begin());
			}

clear()

清空链表数据,删除除了哨兵位的节点即可;

			// 清空链表数据
			void clear()
			{
				iterator it = begin();
				while (it != end())
				{
					it = erase(it);
				}
			}		

以上修改接口配合迭代器的使用如下图:

在这里插入图片描述

在这里插入图片描述

(3)空链表初始化

			// 空链表初始化
			void empty_init()
			{
				_head = new Node;
				_head->_next = _head;
				_head->_prev = _head;
	
				_size = 0;
			}

(4)构造函数

构造函数只需要创建一个哨兵位即可;

			// 构造函数
			list()
			{
				empty_init();
			}

(5)拷贝构造函数

拷贝构造函数直接初始化,然后插入数据即可;

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

(6)赋值运算符重载

现代写法,传参的时候调用拷贝构造,然后交换数据即可;

			// 赋值运算符重载 -- lt2 = lt1
			list<T>& operator=(list<T> lt)
			{
				swap(lt);
	
				return *this;
			}

(7)析构函数

清空链表数据之后再释放哨兵位的节点即可;

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

4. 打印容器的接口

(1)打印链表整型的接口

vectorlist 这些容器都没有重载流插入运算符,所以我们可以自己实现一个打印的接口函数;我们先来实现一下打印链表整型的接口:

		// 打印链表 -- 只能针对 int 类型
		void print_list(const list<int>& lt)
		{
			list<int>::const_iterator it = lt.begin();
			while (it != lt.end())
			{
				//*it = 10; error
				cout << *it << " ";
				++it;
			}
			cout << endl;
		}

此接口可以打印链表的数据,但是只能针对 int 类型,我们可以对它进行改造一下,使用模板。

(2)打印 list 的接口

我们学了模板,就可以利用模板实现泛型编程,将类型改为模板的泛型,即可打印 list 中的不同类型,如下:

		// 打印链表 -- 只能打印 list 容器
		template<typename T>
		void print_list(const list<T>& lt)
		{
			typename list<T>::const_iterator it = lt.begin();
			while (it != lt.end())
			{
				//*it = 10; error
				cout << *it << " ";
				++it;
			}
			cout << endl;
		}

这里的模板参数使用了 typedef 关键字,这里必须使用 typedef 关键字,而且在指定类域前还要加上 typedef 关键字,如 typename list<T>::const_iterator it = lt.begin();;因为在模板还没有进行实例化的时候, const_iterator 就到 list<T> 的类域中寻找类型,此时类中还没有实例化参数 T,所以编译器分不清它是类型还是静态变量,不能去 list<T> 里面找,所以在前面加 typedef 关键字就说明它是个类型,编译器在等 list<T> 实例化后,再去类里面去取根据类型去取类型。

但是上面的接口还是不够完美,要是我想打印 vector 呢?那还是不能打印出来,所以我们可以实现一个专门打印容器的接口;

(3)打印容器的接口

我们使用模板参数代表容器,让编译器到指定容器去取它的迭代器即可;

		// 打印容器 -- 能打印各种容器
		template<typename container>
		void print_container(const container& con)
		{
			typename container::const_iterator cit = con.begin();
			while (cit != con.end())
			{
				cout << *cit << " ";
				++cit;
			}
			cout << endl;
		}

使用如下图:

在这里插入图片描述

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

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

相关文章

七夕特辑——3D爱心(可监听鼠标移动)

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…

第13章——FreeRTOS队列

1.队列简介 队列是任务到任务、任务到中断、中断到任务数据交流的一种机制&#xff08;消息传递&#xff09; FreeRTOS基于队列&#xff0c; 实现了多种功能&#xff0c;其中包括队列集、互斥信号量、计数型信号量、二值信号量、 递归互斥信号量&#xff0c;因此很有必要深入了…

软考A计划-系统集成项目管理工程师-标准规范

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…

python多线程真是让人受够了

一、有8种不同的方法 三、参考文献 https://superfastpython.com/multiprocessing-pool-issue-tasks

linux-进程

1.先谈硬件 冯诺依曼体系结构 一个计算机能够正常运行&#xff0c;就必须遵守冯诺依曼体系 数据流向 为什么不把Cpu直接怼到输入设备和输出设备中间&#xff0c;非要加个内存呢&#xff1f; 答&#xff1a;因为根据木桶原理&#xff0c;如果这样设计&#xff0c;导致最终效…

赴印设厂获得的份额减少,富士康后悔莫及,中国制造获苹果认可

随着iPhone的量产在推进&#xff0c;产业链人士指出iPhone15的分配份额已基本确定&#xff0c;富士康获得了58%的份额&#xff0c;中国大陆的纬创获得28%的份额&#xff0c;而纬创只获得了1%的份额&#xff0c;显示出富士康和纬创这两家企业听从苹果的要求赴印设厂反而被抛弃。…

如何优雅地处理Java多线程编程中的共享资源问题,以确保线程安全和高性能?

文章目录 &#x1f389;欢迎来到Java面试技巧专栏~探索Java中的静态变量与实例变量 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&#x1f379;✨博客主页&#xff1a;IT陈寒的博客&#x1f388;该系列文章专栏&#xff1a;Java面试技巧文章作者技术和水平有限&#xff0c;如果文中出现错误…

2017年3月全国计算机等级考试真题(C语言二级)

2017年3月全国计算机等级考试真题&#xff08;C语言二级&#xff09; 第1题 每个学校有一名校长&#xff0c;且不同学校的校长可以是同一人&#xff0c;则实体学校和实体校长间的联系是 A. 多对一 B. 多对多 C. 一对一 D. 一对多 正确答案&#xff1a;A 第2题 若有以下定义…

听GPT 讲Prometheus源代码--promtool

promtool是Prometheus的一个命令行工具&#xff0c;它提供了一些功能来帮助用户进行Prometheus配置文件&#xff08;如prometheus.yml&#xff09;的检查、规则检查和调试&#xff0c;还可以用于查询Prometheus服务器以获取度量值等。 以下是一些主要的promtool命令&#xff1a…

Azure不可变Blob存储

文章目录 Azure不可变Blob存储介绍Azure不可变性策略实战演练 Azure不可变Blob存储介绍 不可变的存储是一种用于存储业务关键型 Blob 数据的存储方式。与可变存储相反&#xff0c;不可变存储的特点是一旦数据被写入后&#xff0c;便无法再对其进行修改或删除。这种存储方式提供…

一百六十一、Kettle——Linux上安装的kettle9.2开启carte服务(亲测、附流程截图)

一、目的 在Linux上安装好kettle9.2并且连接好各个数据库后&#xff0c;下面开启carte服务 二、实施步骤 &#xff08;一&#xff09;carte服务文件路径 kettle的Linux运行的carte服务文件是carte.sh &#xff08;二&#xff09;修改kettle安装路径下的pwd文件夹里的服务器…

『C语言』数据在内存中的存储规则

前言 小羊近期已经将C语言初阶学习内容与铁汁们分享完成&#xff0c;接下来小羊会继续追更C语言进阶相关知识&#xff0c;小伙伴们坐好板凳&#xff0c;拿起笔开始上课啦~ 一、数据类型的介绍 我们目前已经学了基本的内置类型&#xff1a; char //字符数据类型 short …

会声会影2023旗舰版电脑端视频剪辑软件

随着短视频、vlog等媒体形式的兴起&#xff0c;视频剪辑已经成为了热门技能。甚至有人说&#xff0c;不会修图可以&#xff0c;但不能不会剪视频。实际上&#xff0c;随着各种智能软件的发展&#xff0c;视频剪辑已经变得越来越简单。功能最全的2023新版&#xff0c;全新视差转…

【Vue】Mixin 混入

Vue Mixin 混入 1.简介 混入&#xff08;mixin&#xff09;提供了一种非常灵活的方式&#xff0c;来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项&#xff08;如data、methods、mounted等等&#xff09;。当组件使用混入对象时&#xff0c;所有混入对象的…

微信小程序卡片横向滚动竖图

滚动并不是使用swiper&#xff0c;该方式使用的是scroll-view实现 Swiper局限性太多了&#xff0c;对竖图并不合适 从左往右滚动图片示例 wxml代码&#xff1a; <view class"img-x" style"margin-top: 10px;"><view style"margin: 20rpx;…

XOR Subsequence 2023“钉耙编程”中国大学生算法设计超级联赛(10)hdu7390

Problem - 7390 题目大意&#xff1a;有一个n个数的数组a&#xff0c;对他们的所有非空子序列求异或和得到长度为的数组b&#xff0c;给出b&#xff0c;求a 1<n<18 思路&#xff1a;可以发现&#xff0c;a数组其实是b数组的线性基 &#xff08;线性基详解_Hypoc_的博客…

el-tabs的上方目录栏增加自定义按钮

需求如图&#xff1a;需要在el-tabs的最右侧加一个自定义按钮&#xff0c;它不属于el-tab-pane&#xff0c;而且要在最右侧。这时候就要使用定位来完成这个按钮的显示。 代码结构如下&#xff1a;自定义按钮要与el-tabs的层级并列&#xff0c;然后通过css设置custom-btn的定位…

15、分布式锁简介

一 分布式锁简介 分布式锁&#xff1a;满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的核心思想就是让大家都使用同一把锁&#xff0c;只要大家使用的是同一把锁&#xff0c;那么我们就能锁住线程&#xff0c;不让线程进行&#xff0c;让程序串行执行&#xf…

2023.8-java-基本语法

基本语法 编写 Java 程序时&#xff0c;应注意以下几点&#xff1a; 大小写敏感&#xff1a;Java 是大小写敏感的&#xff0c;这就意味着标识符 Hello 与 hello 是不同的。类名&#xff1a;对于所有的类来说&#xff0c;类名的首字母应该大写。如果类名由若干单词组成&#x…

nginx(七十七)nginx与包体的探究

一 nginx与body体 说明&#xff1a;本文不具有生产意义,只是为了nginx知识的闭环,可以跳过即可 --> "数据脱敏"题外话&#xff1a; 对body的CURD,nginx和openresty处理方式不同强调&#xff1a; 本文是基于http演示的,如果是https加密我们是看不到的 ① core模…