【C++】STL之list容器的模拟实现

news2024/10/5 18:24:43

在这里插入图片描述

在这里插入图片描述

个人主页:🍝在肯德基吃麻辣烫
分享一句喜欢的话:热烈的火焰,冰封在最沉默的火山深处。

文章目录

  • 前言
  • 一、list的三个类的关系分析图
    • vector和list的区别
      • 1.节点的成员变量以及构造函数
      • 2.list的迭代器
  • 二、list的增删查改工作
    • 2.1insert()
    • 2.2erase()
    • 2.3 push_back(),pop_back(),push_front(),pop_front()
    • 2.4clear()
  • 三、list的默认成员函数
    • 3.1 构造函数
    • 2.2 拷贝构造
    • 2.3析构
  • 完整代码
  • 总结


前言

本文章进入C++STL之list的模拟实现。


一、list的三个类的关系分析图

在STL标准库实现的list中,这个链表是一个== 双向带头循环链表==。
在这里插入图片描述

说明:
list是一个类,成员变量为_head
节点类node,是每一个节点。
list的迭代器也升级成了类,成员变量为node。

  • 把迭代器升级成类是为了能够重载++,–,*,!=等可以用在vector迭代器上的操作。

vector和list的区别

vectorlist
底层结构是一块连续的空间不是连续的空间
随机访问支持下标随机访问不支持下标随机访问
插入和删除如果是头插头删,效率为O(n) ,如果插入时需要扩容,会付出更高的代价头插头删都很方便,O(1)的效率
空间利用情况底层为连续的动态空间,内存碎片小,空间利用率高底层是一块一块不连续的空间,内存碎片多,空间利用率较低
迭代器使用天然的原生迭代器将迭代器封装起来再对外开放
迭代器失效在进行插入删除时,插入/删除位置及其之后,空间不再属于自己,由于空间是连续的,其之后的迭代器全部失效在插入时迭代器不会失效,只有在删除时迭代器会失效,且不会影响其他迭代器
使用场景需要进行大量随机访问,需要高效存储,不关心插入删除。大量插入删除操作时,不关心随机访问

vector在内存中是一块连续的地址空间,vector下标就是天然的迭代器。
list在内存中并不连续,所以不支持随机访问。为了能够让list完成诸如vector的操作,比如:

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

我们对list的迭代器进行封装,重载各种操作符,以便完成上述的操作。

1.节点的成员变量以及构造函数

在数据结构之链表中,我们知道一个节点必须包含prev,next,val三个变量,在STL的list也是如此。

template<class T>
//class list_node 如果这样写,节点的所有成员都是私有的,无法直接访问
struct list_node
{
	list_node<T>* _prev;
	list_node<T>* _next;
	T _val;

	//节点的构造函数
	list_node(const T& val = T())
		:_prev(nullptr)
		, _next(nullptr)
		, _val(val)
	{}
};

注意:
1.在C++,struct升级成了类,但如果不标明,struct的所有成员都是公有的。
2.类名不是类型,不能使用list_node*来作为指针的类型。要使用模板来作为类型。

2.list的迭代器

需要注意的一个点:

  • list的迭代器是用来访问的,而不是用来管理节点的空间,所以list的空间是由自己管理和释放的,我们知道,程序崩溃主要就是同一块空间释放两次。
  • 所以list的迭代器在进行拷贝构造时可以使用浅拷贝,不会造成程序的崩溃。
list<int>:: iterator it = lt1.begin();

对这行代码来说,迭代器不是直接赋值给it的,因为迭代器升级成了类,而不是一个指针。这里会调用迭代器的拷贝构造函数。

list迭代器代码如下:

//迭代器也封装起来,形成我们熟悉的*it,++it
template<class T, class Ref, class Ptr>
//class __list_iterator 如果这样写,迭代器是私有的,无法直接使用
//迭代器使用类模板,为了重载Ref,Ptr,设置3个模板参数
struct __list_iterator
{
	typedef list_node<T> Node;
	typedef __list_iterator<T, Ref, Ptr> self;
	//等价于:
	//typedef __list_iterator<T, T&, T*> self;

	Node* _node; //成员

	__list_iterator(Node* node)
		:_node(node)
	{}

	//返回引用,可读可写
	Ref operator*()
	{
		return _node->_val;
	}

	//返回地址
	Ptr operator->()
	{
		return &_node->_val;
	}

	//返回迭代器本身
	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& it) const
	{
		return _node != it._node;
	}

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

};
  • 1.使用三个模板参数的原因:
    • 要求返回指针,如:用指针->访问成员的情况;或者返回迭代器本身的情况,如 ++ ,–操作。
  • 2.在重载operator->()时,返回的是val的地址,所以我们在调用该函数时,需要进行两次->操作才能访问成员变量。
    比如:
struct A
{

	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		, _a2(a2)
	{}

	int _a1;
	int _a2;
};

bit::list<A>lt2;
lt2.insert(lt2.begin(), A(1, 1));
lt2.insert(lt2.begin(), A(2, 2));
list<A>::iterator it = lt2.begin();
while (it != lt2.end())
{
	//迭代器重载了*,返回T&,也就是A本身
	cout << it->_a1 << " " << it->_a1 << endl;
	++it;
}
cout << endl;

本质上,应该需要 it->->_al,it->->_a2才能够访问成员。
第一个it->是调用operator->重载,返回val的地址,第二个->是通过地址访问成员。
编译器为了简化操作,以及看起来没有那么别扭,对it->->_a1进行简化成了it->_a1。

  • 3.类模板中的Ref是Reference,是引用的意思,Ptr是Pointer,是指针的意思。通过这两个模板名可以知道迭代器需要支持&,和*的操作。
  • 4.迭代器被重命名成self,也就是迭代器本身,在++,–操作时,需要返回迭代器本身。

二、list的增删查改工作

2.1insert()

在pos位置之前插入val值。

首先要获取pos位置的前一个节点,记为posprev。
将新的节点插入即可。

iterator insert(iterator pos, const T& val)
{
	Node* newnode = new Node(val);
	Node* inspos = pos._node;
	Node* posprev = inspos->_prev;

	newnode->_next = inspos;
	inspos->_prev = newnode;

	newnode->_prev = posprev;
	posprev->_next = newnode;

	++_size;
	return posprev;

}

list的插入没有迭代器的失效问题。

2.2erase()

删除pos位置的节点,并返回pos位置的下一个位置的迭代器。

iterator erase(iterator pos)
{
	assert(pos != end());
	Node* erapos = pos._node;
	Node* eraprev = erapos->_prev;
	Node* eranext = erapos->_next;

	eraprev->_next = eranext;
	eranext->_prev = eraprev;

	erapos->_prev = erapos->_next = nullptr;
	delete erapos;

	--_size;
	return eranext;
}

erase后pos位置的迭代器会失效,我们需要返回下一个位置的迭代器来防止继续访问出现失效的情况。
在这里插入图片描述
删除后如果迭代器不更新,继续访问会出现野指针问题。

2.3 push_back(),pop_back(),push_front(),pop_front()

这几个函数分别是:尾插,尾删,头插,头删,我们复用insert和erase即可完成。

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

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

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

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

2.4clear()

该函数是将链表的所有节点全部释放,哨兵位头节点除外。

//把所有的节点都释放了,除了哨兵位的头节点
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		//为防止迭代器失效,erase后会返回pos位置的下一个位置,所以it不需要++
		it = erase(it);
	}

	_size = 0;
}

注意:erase()删除pos节点后,会返回pos位置的下一个位置,所以这里不需要++it


三、list的默认成员函数

3.1 构造函数

首先申请一个哨兵位的头节点。

void empty_init()
{
	_head = new Node;
	_head->_prev = _head;
	_head->_next = _head;
	_size = 0;
}

//构造函数,构造出一个链表
list()
{
	empty_init();
}

2.2 拷贝构造

由于我们将迭代器进行了封装升级,现在可以使用++it等操作。
拷贝构造是深拷贝,先new一个_head哨兵位的头节点,再逐个节点进行尾插。

list(list<T>& lt)
{
	empty_init();
	list<T>::iterator it = lt.begin();
	while (it != lt.end())
	{
		push_back(*it);
		++it;
		++_size;
	}

}

2.3析构

调用clear函数释放所有空间,再释放头节点。

~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

完整代码

namespace bit
{
	//c++不喜欢使用内部类,所以把节点类放在list外面
	//节点封装成类
	template<class T>
	//class list_node 如果这样写,节点的所有成员都是私有的,无法直接访问
	struct list_node
	{
		list_node<T>* _prev;
		list_node<T>* _next;
		T _val;

		//节点的构造函数
		list_node(const T& val = T())
			:_prev(nullptr)
			, _next(nullptr)
			, _val(val)
		{}
	};

	//迭代器也封装起来,形成我们熟悉的*it,++it
	template<class T, class Ref, class Ptr>
	//class __list_iterator 如果这样写,迭代器是私有的,无法直接使用
	//迭代器使用类模板,为了重载Ref,Ptr,设置3个模板参数
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> self;
		//等价于:
		//typedef __list_iterator<T, T&, T*> self;

		Node* _node; //成员

		__list_iterator(Node* node)
			:_node(node)
		{}

		//返回引用,可读可写
		Ref operator*()
		{
			return _node->_val;
		}

		//返回地址
		Ptr operator->()
		{
			return &_node->_val;
		}

		//返回迭代器本身
		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& it) const
		{
			return _node != it._node;
		}

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

	};
    
	template<class T>
	class list
	{
		//节点不希望对外开发,把节点封装起来。
		typedef list_node<T> Node;
	public:
		//迭代器对外开放即可访问节点
		//这个迭代器是给链表用的。
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

		iterator begin()
		{
			//return iterator(_head->_next);
			return _head->_next; // 单参数的构造函数可以进行隐式类型转换,从节点的指针转换成一个类
		}

		iterator end()
		{
			//return iterator(_head);
			return _head;
		}

		const_iterator begin() const
		{
			return const_iterator(_head->_next);
			//return _head->_next; // 单参数的构造函数可以进行隐式类型转换,从节点的指针转换成一个类
		}

		const_iterator end() const
		{
			return const_iterator(_head);
			//return _head;
		}

		void empty_init()
		{
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
			_size = 0;
		}

		//构造函数,构造出一个链表
		list()
		{
			empty_init();
		}

		//拷贝构造
		//lt2(lt1)
		list(list<T>& lt)
		{
			empty_init();
			list<T>::iterator it = lt.begin();
			while (it != lt.end())
			{
				push_back(*it);
				++it;
				++_size;
			}

		}

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

		//把所有的节点都释放了,除了哨兵位的头节点
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				//为防止迭代器失效,erase后会返回pos位置的下一个位置,所以it不需要++
				it = erase(it);
			}

			_size = 0;
		}

		void swap(list<T>& tmp)
		{
			std::swap(_head, tmp._head);
			std::swap(_size, tmp._size);
		}

		//赋值运算符重载
		//先调用拷贝构造,再调用赋值重载
		//lt3 = lt1
		//lt1先拷贝构造给tmp
		list<T>& operator=(list<T> tmp)
		{
			swap(tmp);
			//出了作用域,调用析构
			return *this;
		}

		size_t size() const
		{
			return _size;
		}



		//尾插传过来的是一个数据,而不是一个节点
		void push_back(const T& x)
		{
			//Node* tail = _head->_prev;
			调用构造
			//Node* newnode = new Node(x);
			//tail->_next = newnode;
			//newnode->_prev = tail;

			//_head->_prev = newnode;
			//newnode->_next = _head;

			insert(end(), x);
		}

		void pop_back()
		{
			//Node* del = _head->_prev;
			//_head->_prev = del->_prev;
			//del->_prev->_next = _head;

			//del->_next = del->_prev = nullptr;
			//delete del;
			//左闭右开,需要--
			erase(--end());
		}

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

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

		//在pos位置之前插入val
		//不会出现迭代器失效问题
		iterator insert(iterator pos, const T& val)
		{
			Node* newnode = new Node(val);
			Node* inspos = pos._node;
			Node* posprev = inspos->_prev;

			newnode->_next = inspos;
			inspos->_prev = newnode;

			newnode->_prev = posprev;
			posprev->_next = newnode;

			++_size;
			return posprev;

		}
		//迭代器封装成了类,所以需要通过迭代器找到节点
		//防止迭代器失效,返回删除节点的下一个
		iterator erase(iterator pos)
		{
			assert(pos != end());
			Node* erapos = pos._node;
			Node* eraprev = erapos->_prev;
			Node* eranext = erapos->_next;

			eraprev->_next = eranext;
			eranext->_prev = eraprev;

			erapos->_prev = erapos->_next = nullptr;
			delete erapos;

			--_size;
			return eranext;
		}

	private:
		Node* _head;
		size_t _size;
	};
}

总结

本文章完成了list的常用接口的模拟实现。

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

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

相关文章

【数据结构】24王道考研笔记——图

六、图 目录 六、图定义及基本术语图的定义有向图以及无向图简单图以及多重图度顶点-顶点间关系连通图、强连通图子图连通分量强连通分量生成树生成森林边的权、带权网/图特殊形态的图 图的存储及基本操作邻接矩阵邻接表法十字链表邻接多重表分析对比图的基本操作 图的遍历广度…

# Linux下替换文件中的颜色等控制字符的方法

Linux下替换文件中的颜色等控制字符的方法 文章目录 Linux下替换文件中的颜色等控制字符的方法1 Linux下的控制字符&#xff08;显示的文字并不是他本身&#xff09;&#xff1a;2 颜色字符范例&#xff1a;3 替换4 最后 我们在shell编程显示输出时&#xff0c;会定义文字颜色&…

ESD放电模式以及电源箝位 (power clamp )电路

目录 1.ESD的基本概念 2.ESD放电模式与泄放路径 2.1 I/O端与 Vcc或者 I/O端与 Vss 2.2 I/O端与 I/O端 2.3 Vcc&#xff08;电源端&#xff09;与Vss&#xff08;地端&#xff09; 2.4不同类型电压源 3. 电源箝位 (power clamp )电路 4.全芯片ESD保护电路系统框图 参考…

wsl2 kali linux install android studio Android SDK platforms

studio下载网址为 https://developer.android.google.cn/ 解压后在bin目录下运行studio.sh提示错误&#xff1a; unable to access android sdk add-on list 选择“setup proxy” 选择Manual proxy configuration 设置 Host name 为&#xff1a;mirrors.neusoft.edu.cn 设置…

从零开发短视频电商 单元测试(TestNG)

文章目录 简介简单示例执行测试并查看测试报告方式一 在IDEA中运行testng.xml文件方式二 在IDEA中运行测试类或者package方式三 在Maven中运行测试 统计测试覆盖率方式一 IDEA 支持详细的代码测试覆盖率统计方式二 Maven支持测试覆盖率 在IDEA中创建测试用例使用 IDEA 快速创建…

LLaMA微调记录

本文基于开源代码https://github.com/Lightning-AI/lit-llama/tree/main执行微调 其他参考链接&#xff1a; Accelerating LLaMA with Fabric: A Comprehensive Guide to Training and Fine-Tuning LLaMA - Lightning AI 结构化数据示例&#xff1a; BelleGroup/train_0.5M_…

动态sql语句

1.1 动态sql语句概述 Mybatis 的映射文件中&#xff0c;业务逻辑复杂时&#xff0c; SQL是动态变化的&#xff0c;此时在前面的学习中 SQL 就不能满足要求了。 参考的官方文档&#xff1a; 1.2 动态 SQL 之<if> 根据实体类的不同取值&#xff0c;使用不同的 SQL语句…

常见安装工具以及命令(工作常用)长期维护

dockermongodbnginxredis 1.docker 启动2.docker 安装 MongoDB3.启动nginx4.redis配置&#xff0b;安装4.1 Redis的启动和停止4.2 后台启动方式 systemctl start docker redis-server /root/myredis/redis.conf docker start mymongo docker exec -it mymongo /bin/bash 1.doc…

代码随想录算法训练营第58天 | 单调栈 ●739 每日温度 ●496下一个更大元素I ●503下一个更大元素II ●42 接雨水 ●84 柱形图中最大的矩形

#单调栈&#xff1a; 单调栈就是保持栈内元素有序。和栈与队列&#xff08;239. 滑动窗口最大值 自己写一个class来实现单调队列&#xff09;一样&#xff0c;需要我们自己维持顺序&#xff0c;没有现成的容器可以用。 通常是一维数组&#xff0c;要寻找任一个元素的右边或者…

浅谈物联网工程专业:技术融合与未来发展

技术融合与未来发展 引言1. 专业的定义与概述2. 专业的知识体系3. 专业的实践应用4. 专业的发展趋势5. 专业的就业前景结语&#x1f340;小结&#x1f340; &#x1f389;博客主页&#xff1a;小智_x0___0x_ &#x1f389;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收…

zabbix 企业级级监控(1) 监控自己

重点一 Zabbix简介在企业网络运维过程中&#xff0c;管理员必须随时关注各服务器和网络的运行状况&#xff0c;以便及时发现问题&#xff0c;尽可能减少故障的发生。当网络中的设备&#xff0c;服务器等数量较多时&#xff0c;为了更加方便&#xff0c;快捷的获得监控信息&…

【软件测试面试】腾讯数据平台笔试题-接口-自动化-数据库

数据库题 答案&#xff1a; Python编程题 答案&#xff1a; 接口参数化题 答案&#xff1a; 接口自动化题 答案&#xff1a; 以下是我收集到的比较好的学习教程资源&#xff0c;虽然不是什么很值钱的东西&#xff0c;如果你刚好需要&#xff0c;可以评论区&#…

6.3.6 利用Wireshark进行协议分析(六)----网页提取过程的协议分析

6.3.6 利用Wireshark进行协议分析&#xff08;六&#xff09;----网页提取过程的协议分析 利用Wireshark捕获网页访问过程中产生的应用协议报文&#xff0c;还原Web服务中报文的交互过程&#xff0c;为了防止网页直接从本地缓存中获取&#xff0c;我们首先需要清空浏览器保存的…

GO语言GMP模型

目录 程序入口 协程主动让出: 被动让出: schedule 监控线程 程序入口 在执行一系列检查和初始化&#xff08;创建多少个P&#xff0c;与M&#xff10;关联&#xff09;后&#xff0c;进入runtime.main,创建main goroutine,执行mian.mian。 一开始GO语言的调度只有M和G。每个M…

【代码随想录 | Leetcode | 第七天】链表 | 链表相交 | 环形链表 II

前言 欢迎来到小K的Leetcode|代码随想录|专题化专栏&#xff0c;今天将为大家带来链表相交和环形链表 II的分享✨ 目录 前言面试题 02.07. 链表相交142. 环形链表 II总结 面试题 02.07. 链表相交 ✨题目链接点这里 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找…

C/C++ new A与new A()的区别

在C中&#xff0c;POD是“Plain Old Data”的缩写&#xff0c;即“普通旧数据”。POD data是指一种特殊类型的数据结构&#xff0c;它们具有简单的内存布局&#xff0c;没有构造函数、虚函数、私有/保护非静态数据成员&#xff0c;也没有虚继承等特性。这些数据结构可以直接通过…

k8s与集群管理

从docker讲起 终于有人把 Docker 讲清楚了&#xff0c;万字详解&#xff01; Docker资源&#xff08;CPU/内存/磁盘IO/GPU&#xff09;限制与分配指南 默认情况下&#xff0c;Docker容器是没有资源限制的&#xff0c;它会尽可能地使用宿主机能够分配给它的资源。如果不对容器资…

C++--day3(内联函数、结构体、类、封装、this、构造函数、析构函数)

#include <iostream>using namespace std;class My_stack { private:int *ptr; //指向堆区空间int top; //记录栈顶元素int size; public://有参构造My_stack(int size):ptr(new int[size]),top(-1){this->sizesize;cout<<"My_stack::有参构造&…

基于STM32的智能喂养系统

基于STM32的智能喂养系统 系统简介 自动检测环境温湿度&#xff0c;当温湿度低于阈值时自动打开加湿器&#xff1b;自动检测水位&#xff0c;当水位低于阈值时自动加水&#xff1b;自动检测有害气体&#xff0c;当检测到有害气体时自动打开风扇&#xff1b;同步状态到微信小程…

中间件上云部署 zookeeper

中间件上云部署 zookeeper 企业级中间件上云部署 zookeeper一、环境说明二、zookeeper部署YAML资源清单准备三、zookeeper部署及部署验证四、zookeeper应用验证 企业级中间件上云部署 zookeeper 一、环境说明 storageclassingress 二、zookeeper部署YAML资源清单准备 # vim…