STL——list的介绍和模拟实现

前言

本篇博客我们将要开始介绍list这个容器,list是带头双向循环链表,STL标准模板库中实现了list这样方便我们去使用,那么本篇博客我们将脱下list的神秘外衣,介绍它的使用以及模拟实现。

list的介绍

list的底层是带头双向循环链表,可以在任意位置完成插入删除的功能,了解其迭代器我们将更加深入了解C++封装的特性。与vector相比,list的最大缺陷是不支持随机访问,但由于其底层封装了迭代器,因此list是可以通过迭代器访问的。同时,由于list的结构是链状的,所以它在一定程度上可以节省空间

下面我们来看一下list为我们提供的接口:

首先来看list的构造函数:

list与其他容器一样都提供了一个迭代器构造,initializer构造以及拷贝构造 

接下来是迭代器:

 容量操作:

修改操作:

 

list的模拟实现

这是这篇博客的重点,通过模拟实现list我们将更加深入了解list的底层结构,以及如何使用

节点:

首先我们提供一个节点的类,这个类里面包含了prev指针,next指针以及我们要存储的数据data,如下所示:

template<class T> 
struct ListNode
{
	ListNode<T>* _next;
	ListNode<T>* _prev;
	T _data;
	ListNode(const T& data = T()) :_next(nullptr), _prev(nullptr), _data(data);
	{}
};

分析上述代码,我们看到,我们将这个类声明为struct而不是class,这是为什么呢?因为在后续的操作过程中我们将频繁访问 节点当中的数据,如果声明成class,那么如果我们要访问内部的数据就要声明成友元或者内部类,这样会对阅读代码的人造成困扰,同时也不方便我们去操作,所以在实际操作过程中,如果看见了需要频繁操作的数据,那么我们尽可能的让它声明成struct

同时,我们在这个节点类中提供了一个构造函数,这样可以使我们的程序更加安全可靠。

list类

有了节点后我们将用这个节点去创建一个list,因此我们此时需要一个list类,里面封装我们要实现list的各种操作,其代码如下所示:

template<class T>
class list
{
	typedef ListNode<T> Node;
public:

private:
	Node* _head;
};

首先我们搭建出list的框架,里面封装了一个头节点,其后续要实现的各种功能我们将在public中实现。同时,我们将ListNode<T>重新命名为了Node,这样是为了可读性考虑

迭代器的实现

我们知道,迭代器为容器提供了一种统一的访问形式,从而屏蔽了底层细节,方便人们使用和学习。前面我们实现了vector,它的迭代器是一个指针,那么对于list而言我们能用指针实现吗?答案是否定的,因为它的结构不是连续的,所以我们不能这么做,那么为了保证我们有一种统一的访问方式我们必须提供一个类去封装iterator,这个类里面得重载前置++,前置--,后置++,后置--,迭代器,解引用等功能,其结构如下所示:

	template<class T>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T> Self;
		Node* _node;

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


	};

分析上述代码,我们可以看到,在这个迭代器类中我们保存了一个节点,以便访问节点中的数据,从而实现对节点的操作,由于后续要频繁访问数据,所以我们同样将ListIterator声明为公有。

前置++/--

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

后置++/--

Self operator++(int)
{
	Self tmp(_node);
	_node = _node->_next;
	return tmp;
}
Self operator--(int)
{
	Self tmp(_node);
	_node = _node->_prev;
	return tmp;
}

重载*

T& operator*()
{
	return _node->_data;
}

重载->

T* operator->()
{
	return &_node->_data;
}

重载==/!=

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

 由此,我们就完成了一个iterator,但是这样的代码显然移至性是很低的,为什么呢?因为在实际操作过程中,我们常常会遇见const修饰的对象,那么对于其而言我们应该使用const迭代器去完成,那这该怎么办呢?有两种解决方法,一种是重新实现一个const类型的迭代器;另一种通过调用不同的模板参数去完成,显然第一种方法虽然很简单,但是有很多重复的代码,所以我们在实际操作中更多的是用第二种解决方法。

仔细观察上述代码,我们发现只有函数可以用const修饰,一个是T&,一个是T*,那么我们可以再加两个模板参数,一个负责T&,一个负责T*,那么我们就得到了下面的代码:

template<class T,class Ref,class Ptr>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T,T&,T*> Self;
	Node* _node;

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

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

	Self operator++(int)
	{
		Self tmp(_node);
		_node = _node->_next;
		return tmp;
	}
	Self operator--(int)
	{
		Self tmp(_node);
		_node = _node->_prev;
		return tmp;
	}

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

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

begin()与end()

当我们将list的迭代器类实现出来后我们就可以用迭代器访问这个类,因此我们应该在list这个类中提供一个接口,这个接口就是begin与end,其代码如下所视:

typedef ListIterator<T, T&, T*> iterator;
iterator begin()
{
	return iterator(_head->_next);
}
iterator end()
{
	return iterator(_head);
}
typedef ListIterator<T, const T&,const T*> const_iterator;
const_iterator begin()const
{
	return const_iterator(_head->_next);
}
const_iterator end()const
{
	return const_iterator(_head);
}

list的插入/删除

list最常见的插入删除就是在头部和尾部的插入删除,其中插入的步骤是:

1.创建新节点

2.获取目标位置的节点及其前驱节点

3.让新节点的头指向目标位置节点的头节点,让新节点的尾节点指向目标位置节点

4.让前驱节点的尾指向新节点,目标位置节点的头指向新节点

值得注意的是:list的插入/删除伴随着迭代器失效的问题,即:插入新节点后无法访问插入位置的数据

那么解决方法是什么呢?很简单,就是返回插入位置的数据,因此list的插入和删除都要传迭代器接收目标位置的数据,代码如下:

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

	newnode->_next = cur;
	newnode->_prev = prev;
	prev->_next = newnode;
	cur->_prev = newnode;
	return iterator(newnode);
}
iterator erase(iterator pos)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

	prev->_next = next;
	next->_prev = prev;
	delete cur;
	return iterator(next);
}

有了插入删除的接口,那么我们就可以很轻松实现list的头部插入删除,尾部插入删除,其代码如下:

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

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

构造/析构函数

list的C++标准库中提供了很多的构造方式,我们在这里只着重介绍其中的无参构造,initializer_list构造以及拷贝构造,在实现构造函数之前,我们应该先写一个初始化函数,这个初始化函数是将头节点的next和prev都指向自己从而实现初始化,我们在写构造函数时,主要用这个初始化函数来构造,其代码如下:

void empty_init()
{
	_head = new Node();
	_head->_next = _head;
	_head->_prev = _head;
}
list()
{
	empty_init();
}
list(const list<T>& il)
{
	empty_init();
	for (const auto& e : il)
	{
		push_back(e);
	}
}
list(initializer_list<T> il)
{
	empty_init();
	for (const auto& e : il)
	{
		push_back(il);
	}
}

析构函数与构造函数类似,提供了一个clear接口,通过调用clear接口逐个删除list的节点,最后再将头节点释放,这样就实现了一个简单的析构函数,其代码如下所示:

void clear()
{
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}
~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

重载=

list的重载=是通过传值传参然后调用swap来实现的,其代码如下所示:

list<T>& operator=(const list<T> lt)
{
	swap(_head, lt._head);
	return *this;
}

分析上述代码,我们可以看到,不同于传统的重载operator=我们这里采用了传值传参,然后交换头节点,这样做的好处显而易见,传值传参调用拷贝构造产生一个临时对象,临时对象具有常量属性因此要加const修饰,在出作用域之后就析构销毁。

list模拟实现代码如下

template<class T> 
struct ListNode
{
	ListNode<T>* _next;
	ListNode<T>* _prev;
	T _data;
	ListNode(const T& data = T()) :_next(nullptr), _prev(nullptr), _data(data)
	{}
};

template<class T,class Ref,class Ptr>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T,T&,T*> Self;
	Node* _node;

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

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

	Self operator++(int)
	{
		Self tmp(_node);
		_node = _node->_next;
		return tmp;
	}
	Self operator--(int)
	{
		Self tmp(_node);
		_node = _node->_prev;
		return tmp;
	}

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

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

template<class T>
class list
{
	typedef ListNode<T> Node;
public:
	typedef ListIterator<T, T&, T*> iterator;
	iterator begin()
	{
		return iterator(_head->_next);
	}
	iterator end()
	{
		return iterator(_head);
	}
	typedef ListIterator<T, const T&,const T*> const_iterator;
	const_iterator begin()const
	{
		return const_iterator(_head->_next);
	}
	const_iterator end()const
	{
		return const_iterator(_head);
	}
	void empty_init()
	{
		_head = new Node();
		_head->_next = _head;
		_head->_prev = _head;
	}
	list()
	{
		empty_init();
	}
	list(const list<T>& il)
	{
		empty_init();
		for (const auto& e : il)
		{
			push_back(e);
		}
	}
	list(initializer_list<T> il)
	{
		empty_init();
		for (const auto& e : il)
		{
			push_back(il);
		}
	}

	list<T>& operator=(const list<T> lt)
	{
		swap(_head, lt._head);
		return *this;
	}
	void clear()
	{
		auto it = begin();
		while (it != end())
		{
			it = erase(it);
		}
	}
	~list()
	{
		clear();
		delete _head;
		_head = nullptr;
	}

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

		newnode->_next = cur;
		newnode->_prev = prev;
		prev->_next = newnode;
		cur->_prev = newnode;
		return iterator(newnode);
	}
	iterator erase(iterator pos)
	{
		Node* cur = pos._node;
		Node* prev = cur->_prev;
		Node* next = cur->_next;

		prev->_next = next;
		next->_prev = prev;
		delete cur;
		return iterator(next);
	}

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

	void push_back(const T& x)
	{
		insert(begin(), x);
	}
	void push_front(const T& x)
	{
		insert(end, x);
	}
private:
	Node* _head;
};

小结

本篇博客介绍了list的使用和模拟实现,通过对list的模拟实现,我们介绍了迭代器失效的问题以及迭代器的封装。

感谢您在百忙之中能够看完本篇文章,如果本篇文章对您有所帮助的话,希望您能留下点赞评论加关注,您的支持就是我创作的最大动力。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

飞鱼动画笔记

1.鱼身体&#xff1a;左右移动先转动身体&#xff08;与飞机类似&#xff09; 2.眼睛/嘴巴&#xff1a;绿色等腰三角形的底边和顶点&#xff0c;就是眼睛骨骼旋转弧线经过点 3.鱼鳍和鱼尾&#xff1a;使用springmagic插件制作波浪动画再微调 4.腹部

全面了解机器学习:回归、分类、分割与检测任务

在机器学习的广袤天地中&#xff0c;回归任务和分类任务构成了基础的两大支柱&#xff0c;而分割任务与检测任务则是在此基础上衍生出的重要应用方向。 机器学习的基础任务 回归任务 回归预测是监督学习中的一个重要任务&#xff0c;旨在预测连续数值。线性回归是最简单和最…

【论文阅读笔记】SL-YOLO(2025/1/13) | 小目标检测 | HEPAN、C2fDCB轻量化模块

目录 摘要 1 引言 2 相关工作 3 方法 3.1 为小目标检测增加一个头 3.2 优化网络结构 3.3 改进轻量化模块 3.3.1 C2fDCB 3.3.2 SCDown 4 实验 4.1 数据集 4.2 实验环境 4.3 与其他模型的比较 4.4 消融研究 ▲不同网络结构的分析 ▲不同模块的分析 ▲不同降采样…

进化算法和智能控制国际学术研讨会(ISEAIC 2025)

重要信息 官网&#xff1a;www.icaace.net&#xff08;了解参会投稿等&#xff09; 时间&#xff1a;2025年3月21-23日 地点&#xff1a;中国-上海-上海古井假日酒店 简介 2025进化算法和智能控制国际学术研究会议&#xff08;ISEAIC 2025&#xff09;是2025第八届先进算法…

SpringAI 调用本地ollama大模型

pom文件 <?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://maven.apache.org/POM/4.0.0…

【二.提示词工程与实战应用篇】【1.提示词工程入门:AI对话的艺术】

大家好,今天咱们来聊聊一个特别有意思的话题——提示词工程。你可能已经听说过这个词,或者在使用AI工具时不经意间接触过它。但提示词工程到底是什么?它为什么这么重要?咱们今天就来深入探讨一下,看看它是如何影响我们与AI的对话,以及如何在实际应用中发挥作用的。 什么…

C++:类和对象(下篇)

1. 再谈构造函数 1.1 构造函数体赋值 在创建对象时&#xff0c;编译器通过调用构造函数&#xff0c;给对象中各个成员变量一个合适的初始值。 class Date { public:Date(int year, int month, int day){_year year;_month month;_day day;} private:int _year;int _mont…

【计算机网络】考研复试高频知识点总结

文章目录 一、基础概念1、计算机⽹络的定义2、计算机⽹络的目标3、计算机⽹络的组成4、计算机⽹络的分类5、计算机⽹络的拓扑结构6、计算机⽹络的协议7、计算机⽹络的分层结构8、OSI 参考模型9、TCP/IP 参考模型10、五层协议体系结构 二、物理层1、物理层的功能2、传输媒体3、 …

【Python 数据结构 5.栈】

目录 一、栈的基本概念 1.栈的概念 2.入栈 入栈的步骤 3.出栈 出栈的步骤 4.获取栈顶元素 获取栈顶元素的步骤 二、 Python中的栈 顺序表实现 链表实现 三、栈的实战 1.LCR 123. 图书整理 I 思路与算法 2.LCR 027. 回文链表 思路与算法 3.1614. 括号的最大嵌套深度 思路与算法 …

C++基础算法:模拟

文章目录 1.[P1067 [NOIP 2009 普及组\] 多项式输出 - 洛谷](https://www.luogu.com.cn/problem/P1067)题目解析算法解析代码实现 2.[P5731 【深基5.习6】蛇形方阵 - 洛谷](https://www.luogu.com.cn/problem/P5731)题目解析算法原理代码实现 3.[P1098 [NOIP 2007 提高组\] 字符…

关于对机器中的人工智能进行基准测试

大家读完觉得有帮助记得及时关注和点赞&#xff01;&#xff01;&#xff01; 抽象 最近的基准研究声称&#xff0c;AI 在各种认知任务上的表现已经接近甚至超过人类的“水平”。然而&#xff0c;本立场文件认为&#xff0c;当前的 AI 评估范式不足以评估类似人类的认知能力。我…

c++ 内存管理系统之智能指针

1.c内存管理 1.代码区 也称Text Segment&#xff0c;存放可执行程序的机器码。 2 数据区&#xff1a; 存放已初始化的全局和静态变量&#xff0c; 常量数据&#xff08;如字符串常量&#xff09;。 存放未初始化的全局和静态变量 无疑解释静态变量的来源&#xff1a; 局…

Unity中的Destroy和DestroyImmediate的区别是什么?

在 Unity 中&#xff0c;Destroy 和 DestroyImmediate 都是用于销毁游戏对象&#xff08;GameObject&#xff09;、组件&#xff08;Component&#xff09;或资源的方法。在大多数情况下&#xff0c;建议优先使用 Destroy 方法&#xff0c;只有在确实需要立即销毁对象时才使用 …

Microk8s Ingress实现七层负载均衡

Microk8s Ingress是什么 Ingress是k8s的一种资源对象&#xff0c;用于管理外部对集群内服务的访问, 它通过提供一个统一的入口点&#xff0c;将外部流量路由到集群内部的不同服务。 Microk8s Ingress用于解决什么问题 k8s集群中服务默认只能在集群内访问。 如果需要从外部访…

DeepSpeek服务器繁忙?这几种替代方案帮你流畅使用!(附本地部署教程)

作者&#xff1a;后端小肥肠 目录 1. 前言 2. 解决方案 2.1. 纳米AI搜索&#xff08;第三方平台&#xff09; 2.2. Github&#xff08;第三方平台&#xff09; 2.3. 硅基流动&#xff08;第三方API&#xff09; 3. 本地部署详细步骤 3.1. 运行配置需求 3.2. 部署教程 4…

【大厂AI实践】美团:美团智能客服核心技术与实践

【大厂AI实践】美团&#xff1a;美团智能客服核心技术与实践 &#x1f31f; 嗨&#xff0c;你好&#xff0c;我是 青松 &#xff01; &#x1f308; 自小刺头深草里&#xff0c;而今渐觉出蓬蒿。 NLP Github 项目推荐&#xff1a; 【AI 藏经阁】&#xff1a;https://gitee.com…

科技查新有不通过的情况吗?为什么?

1. 科技查新有不通过的情况吗&#xff1f;为什么&#xff1f; 有。科技查新“不通过”&#xff08;即查新报告显示技术缺乏新颖性或存在侵权风险&#xff09;的情况并不罕见&#xff0c;主要原因包括&#xff1a; &#xff08;1&#xff09;技术缺乏创新性 重复开发&#xff…

批量提取 Word 文档中的页面

如何将 Word 文档中的页面提取出来形成一个新的文档呢&#xff1f;比如将 Word 文档中的第一页提取出来、将 Word 文档中的最后一页提取出来、再或者将 Word 文档中的中间几页提取出来等等。人工的处理肯定非常的麻烦&#xff0c;需要新建 Word 文档&#xff0c;然后将内容复制…

Spring统一格式返回

目录 一&#xff1a;统一结果返回 1&#xff1a;统一结果返回写法 2&#xff1a;String类型报错问题 解决方法 二&#xff1a;统一异常返回 统一异常返回写法 三&#xff1a;总结 同志们&#xff0c;今天咱来讲一讲统一格式返回啊&#xff0c;也是好久没有讲过统一格式返…

(十 八)趣学设计模式 之 观察者模式!

目录 一、 啥是观察者模式&#xff1f;二、 为什么要用观察者模式&#xff1f;三、 观察者模式的实现方式四、 观察者模式的优缺点五、 观察者模式的应用场景六、 总结 &#x1f31f;我的其他文章也讲解的比较有趣&#x1f601;&#xff0c;如果喜欢博主的讲解方式&#xff0c;…