【C++初阶】STL详解(八)List的模拟实现

本专栏内容为:C++学习专栏,分为初阶和进阶两部分。 通过本专栏的深入学习,你可以了解并掌握C++。

💓博主csdn个人主页:小小unicorn
⏩专栏分类:C++
🚚代码仓库:小小unicorn的代码仓库🚚
🌹🌹🌹关注我带你学习编程知识

STL详解(八)

  • list的再认识:
  • 初始化与定义节点:
  • 迭代器实现:
    • 构造:
    • ++
    • 解引用:*
    • !=
    • 基本框架搭建:
    • --
    • 后置++与后置--
    • ->
    • ==
    • const迭代器
      • 拓展:
      • 拓展2:
  • 相关函数接口:
    • Insert:
    • erase:
    • push_front与pop_fronr
    • push_back与pop_back
    • size:
    • clear与析构:
    • 拷贝构造:
    • 赋值重载:
      • 传统写法:
      • 现代写法:
  • 对比vector与list

list的再认识:

在之前List的介绍与使用中,我们知道list容器是一个带头双向循环链表,那么我们在模拟实现中,能不能先证明一下List是否是一个双向循环链表呢?

我们参考一下stl中list的源码:
在这里插入图片描述
我们看到,在源码中,list中有一个__list_node的节点,我们将这个链表的节点定义打开发现定义两个指针next,prev.

再来看一下它的空初始化:
在这里插入图片描述
通过观察源码中list的初始化,确实是一个双向循环链表。

接下来。我们就来自己实现一下里面的接口函数。
注意:在模拟实现中,我们采取用与与源码中相同的命名风格。
为防止与库里面的list重复,我们模拟实现将定义在自己的命名空间中。

初始化与定义节点:

首先,我们需要定义三个类,并用摸版进行封装:分别是list,list的节点,以及迭代器:

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

list:

template<class T>
class list
{
	typedef list_node<T> Node;
public:
	//空初始化:
	void empty_init()
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}
	//无参构造:
	list()
	{
		empty_init();
	}

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

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

		newnode->_next = _head;
		_head->_prev = newnode;
	}
private:
	Node* _head;
};

这里我们写的是无参构造,以及实现了一个尾插接口:
尾插双向链表实现已经再简单不过了:
在这里插入图片描述
现在我们测试一下:
在这里插入图片描述
现在还不能进行遍历,因此我们自己要实现一个迭代器:

迭代器实现:

那么这个迭代器我们要作为怎么实现呢?
我们可以先回顾一下,在vector中,我们实现迭代器就是实现原生指针。
在这里插入图片描述
在vector中,给it解引用就可以访问到里面的数据,但是链表不行,因为链表中空间不是连续的。
那么应该怎么实现呢?其实这个就和我们之前的日期类一样,在日期类中我们用运算符重载与封装实现了对日期类的++操作。而我们的迭代器也使这样实现的。

这里我们需要实现迭代器的!=,*与++操作:
在这里插入图片描述
我们先看一下库里面的操作:
在这里插入图片描述

构造:

看一下库里面的操作:
在这里插入图片描述
库里面用了一个节点的指针进行构造,这是因为:单参数的构造函数支持隐式类型转换。

所以我们的构造就可以这样写:

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

++

实现迭代器++,就是指针往后走的过程,注意返回的是迭代器。我们可以将迭代器重命名一下:

typedef __list_iterator<T> self;

实现代码:

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

解引用:*

返回节点里面的数据即可:

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

!=

两个迭代器进行比较,实质两个指针比较。

//两个迭代器进行比较,两个指针比较
bool operator!=(const self& s)
{
	return _node  !=  s._node;
}

基本框架搭建:

这样基本上迭代器的基本架子就完成了:

typedef __list_iterator<T> iterator;
iterator begin()
{
	return _head->_next;
}
iterator end()
{
	return _head;
}

在list中定义一下迭代器。迭代器开始位置就是返回哨兵位头节点的下一个位置,结束位置就是返回哨兵位的地址。

测试一下:

void test_list1()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);
		list<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
			//*it += 20;
			cout << *it << " ";
			++it;
		}
		cout << endl;
}

测试结果:
在这里插入图片描述
有了迭代器就有范围for:

for (auto e : lt)
{
	cout << e << " ";
}
cout << endl;

在这里插入图片描述
总结:其实会发现就是在模拟指针,但他的底层细节很大。所以迭代器体现了封装的强势之处。封装屏蔽底层差异和实现细节,提供统一的访问修改遍历方式。这样我们就不用关注他的底层是什么.
在这里插入图片描述

举个例子:

	set<int> s;
	s.insert(1);
	s.insert(3);
	s.insert(2);
	s.insert(4);

	set<int>::iterator sit = s.begin();
	while (sit != s.end())
	{
		cout << *sit << " ";
		++sit;
	}
	cout << endl;
}

在这里插入图片描述
这里的set就是树,我们也可以依然用这个迭代器进行遍历。

实现迭代器++,就是指针往前走的过程,注意返回的是迭代器。

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

后置++与后置–

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

->

在讲->重载之前,先看一下这个示例:

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

	int _a1;
	int _a2;
};
list<AA> lt;
lt.push_back(AA(1, 1));
lt.push_back(AA(2, 2));
lt.push_back(AA(3, 3));

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

在这里就访问不了,因为自定义类型不支持类型。

这里我们回顾一下之前的知识,对与内置类型的指针,我们可以采用*来进行解引用。对于自定义类型的指针,我们要用->来进行解引用。

int* p = new int;
*p = 1;

AA* ptr = new AA;
ptr->_a1 = 1;

实现->

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

==

两个迭代器进行比较,就是两个指针比较

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

const迭代器

在实现const迭代器之前,首先要知道一点,const迭代器是一个完全不一样的类,所以不能将非const迭代器前加const就变成const迭代器。
在这里插入图片描述
因此我们可以list类中在定义一个const迭代器:

typedef __list_const_iterator<T> const_iterator;
const_iterator begin() const
{
	return _head->_next;
}
const_iterator end() const
{
	return _head;
}

在单独实现一个const迭代器的类:

template<class T>
struct __list_const_iterator
{
   ....
}

const迭代器基本的功能与非const迭代器相似,只有在解引用时不同:

// *it = 10
const T& operator*()
{
	return _node->_data;
}

// it->a1 = 10
const T* operator->()
{
	return &_node->_data;
}

测试一下:

void print_list(const list<int>& lt)
{
	list<int>::const_iterator it = lt.begin();
	while (it != lt.end())
	{
		//*it = 10;
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

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

	print_list(lt);
}

在这里插入图片描述
但是这样实现,还是太冗余了,因为非const和const只有返回值不同,那么还有优化的空间吗?
我们看一下库里面的实现:
在这里插入图片描述
库里面定义只定义了同一个类模版的迭代器,只是这两个迭代器之间的摸版参数不同,实例化的参数不同,就是完全不一样的类型。也就是说把能靠模版实现的就写一份,让编译器搞。

所以我们可以将我们的迭代器进行进一步优化:

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;
	....
}
// T T& T*
// T cosnt T& const T*
template<class T, class Ref, class Ptr>
struct __list_iterator
{
	typedef list_node<T> Node;
	/*typedef __list_iterator<T> self;*/
	typedef __list_iterator<T, Ref, Ptr> self;
	Node* _node;
	...
}

到这里,我们的迭代器就全部实现完了。

拓展:

在刚才的测试函数中,有一个print_list函数,但是这个测试函数里面的数据我们给定的是int,那我要是其他类型的呢,这个函数又该如何修改呢?
其实很简单:我们加一个摸版参数即可:

template<typename T>
void print_list(const list<T>& lt)
{
	typename list<T>::const_iterator it = lt.begin();
	while (it != lt.end())
	{
		//*it = 10;
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

测试一下:
在这里插入图片描述
注意:这里我们没有用class这个摸版参数,这是因为:

list是一个未实例化的类模板,编译器不能去他里面去找
编译器就无法确定:list::const_iterator是内嵌类型,还是静态成员变量
前面加一个typename就是告诉编译器,这里是一个类型,等list实例化后再去类里面去取

拓展2:

如果要是将刚才的类在改造一下呢?
比如:
我要打印以下内容:

vector<string> v;
v.push_back("222222222222222222222");
v.push_back("222222222222222222222");
v.push_back("222222222222222222222");
v.push_back("222222222222222222222");

这个函数对于我们的printf_list就不适用了,因为我们的print_list就只适用于链表。
这里我们就可以写一个容器(container)的打印函数:

template<typename Container>
void print_Container(const Container& con)
{

	typename Container::const_iterator it = con.begin();
	while (it != con.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

测试结果:
在这里插入图片描述
总结一下:
摸版实现了泛型编程,而泛型编程的本质,就是本来我们干的活,交给了编译器。

相关函数接口:

有了迭代器的实现,我们就可以实现一下链表的相关接口:

Insert:

Insert:在pos位置之前插入:

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

	Node* prev = cur->_prev;

	// prev newnode cur
	prev->_next = newnode;
	newnode->_prev = prev;
	
	newnode->_next = cur;
	cur->_prev = newnode;
	
	++_size;
	return iterator(newnode);
}

Insert迭代器不会产生失效的问题,因为没有扩容。

erase:

在指定位置删除:

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

			delete cur;
			prev->_next = next;
			next->_prev = prev;

			--_size;
			return iterator(next);
}

注意:erase的迭代器会失效,所以我们加个返回值。

实现了insert和erase后,我们就可以服用来实现头插,头删,尾插,尾删。

push_front与pop_fronr

具体实现:

头插:

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

头删:

//头删
void pop_front()
{
	erase(begin());
}

push_back与pop_back

具体实现:

尾插:

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

尾删:

//尾删
void pop_back()
{
	erase(--end());
}

size:

为了方便计算大小,我们还可以再实现一个函数:

size_t size()
{
	return _size;
}

clear与析构:

clear:清理空间,我们可以采取迭代器访问的方式,逐个将节点释放。

//清理空间:
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

析构:我们可以先清理空间,在将头节点释放即可。

//析构
~list()
{
	clear();

	delete _head;
	_head = nullptr;
}

拷贝构造:

我们可以采用范围for来进行拷贝构造:

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

赋值重载:

传统写法:

list<int>& operator=(const list<int>& lt)
{
	if (this != &lt)
	{
		clear();
		for (auto e : lt)
		{
			push_back(e);
		}
	}

	return *this;
}

现代写法:

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

// lt3 = lt1
list<int>& operator=(list<int> lt)
{
	swap(lt);

	return *this;
}

对比vector与list

在这里插入图片描述

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

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

相关文章

2018年2月16日 Go生态洞察:Go 1.10版本发布分析

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

Autosar MCAL-RH850P1HC-MCAL配置环境搭建

文章目录 前言下载安装包软件安装安装SIP包安装MCAL文件配置工程配置生成代码测试静态代码路径总结前言 对于RH850P1HC,官网有免费的MCAL,但官网的MCAL没有CAN模块(原厂反馈为Bosch IP,CAN Driver他们没有),也没有FEE模块。如果需要,可以找第三方软件公司,如ETAS.虽然M…

1.3 取反器和8位取反器

取反器真值表: 取反开关输入输出011000110101 取反器相当于一个异或门 8位取反器

bit_set位图|布隆过滤器

位图 对于海量整形数据的处理&#xff0c;通常是上百个G的代码。 通常有如下的应用&#xff1a; 1. 快速查找某个数据是否在一个集合中 2. 排序 去重 3. 求两个集合的交集、并集等 4. 操作系统中磁盘块标记 如果将数据加载到内存中&#xff0c;运用基本数据结构处理&…

【Python】用三种方法创建tkinter桌面窗口

Python的tkinter是Python的标准GUI库之一&#xff0c;它是一个开源的、跨平台的GUI工具包&#xff0c;可以用于创建桌面应用程序。 tkinter提供了许多常见的GUI组件&#xff0c;例如按钮、文本框、标签、列表框等等&#xff0c;可以轻松地创建各种类型的桌面应用程序。它还支持…

计算机组成原理-Cache的基本概念和原理

文章目录 存储系统存在的问题Cache的工作原理局部性原理性能分析例题界定何为局部部分问题总结 存储系统存在的问题 增加Cache层来缓和CPU和主存的工作速度矛盾 Cache的工作原理 启动某个程序后&#xff0c;将程序的代码从辅存中取出放入内存中&#xff0c;再从内存中将代码…

ArcGIS中基于人口数据计算人口密度的方法

文章目录 一、密度分析原理二、点密度分析三、线密度分析四、核密度分析一、密度分析原理 密度分析是指根据输入的要素数据集计算整个区域的数据聚集状况,从而产生一个联系的密度表面。通过密度计算,将每个采样点的值散步到整个研究区域,并获得输出栅格中每个像元的密度值。…

电子学会C/C++编程等级考试2023年03月(二级)真题解析

C/C++等级考试(1~8级)全部真题・点这里 第1题:数字字符求和 请编写一个程序实现以下功能:从一个字符串中,提取出所有的数字字符即0-9,并作为数求和。 时间限制:1000 内存限制:65536输入 一行字符串,长度不超过100,字符串中不含空格。输出 字符串中所有数字字符作为数…

Java核心知识点整理大全14-笔记

Java核心知识点整理大全-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全2-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全3-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全4-笔记-CSDN博客 Java核心知识点整理大全5-笔记-CSDN博客 Java核心知识点整理大全6…

SpringBoot 2 系列停止维护,Java8 党何去何从?

SpringBoot 2.x 版本正式停止更新维护&#xff0c;官方将不再提供对 JDK8 版本的支持 SpringBoot Logo 版本的新特性 3.2 版本正式发布&#xff0c;亮点包括&#xff1a; 支持 JDK17、JDK21 版本 对虚拟线程的完整支持 JVM Checkpoint Restore&#xff08;Project CRaC&…

2023-11-25 LeetCode每日一题(二叉树中的伪回文路径)

2023-11-25每日一题 一、题目编号 1457.二叉树中的伪回文路径二、题目链接 点击跳转到题目位置 三、题目描述 给你一棵二叉树&#xff0c;每个节点的值为 1 到 9 。我们称二叉树中的一条路径是 「伪回文」的&#xff0c;当它满足&#xff1a;路径经过的所有节点值的排列中…

python中模块的创建及引用(import as,import,from)

模块&#xff08;module&#xff09;简介&#xff1a; 1.模块化&#xff0c;模块化指将一个完整的程序分解为一个一个的小模块&#xff0c; 通过将模块组合&#xff0c;来搭建出一个完整的程序 2.不采用模块化就是统一将所有的代码编写到一个文件中&#xff0c;采用 模块化就是…

SpringBoot——LiteFlow引擎框架

优质博文&#xff1a;IT-BLOG-CN 一、LiteFlow 简介 LiteFlow是一个轻量且强大的国产规则引擎框架&#xff0c;可用于复杂的组件化业务的编排领域。帮助系统变得更加丝滑且灵活。利用LiteFlow&#xff0c;你可以将瀑布流式的代码&#xff0c;转变成以组件为核心概念的代码结构…

jQuery实现横版手风琴效果

一、实现效果 当鼠标滑过方块的时候&#xff0c;方块的状态就会发生如下图所示的变化&#xff0c;同理当鼠标滑到其他的方块也会发生同样的效果&#xff0c;不仅大小会改变同时方块的颜色也会跟着发生变化&#xff1a; 二、代码实现 <!DOCTYPE html> <html><h…

Python pandas数据分析

Python pandas数据分析&#xff1a; 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c;oracle&#xff0c;尤其…

C语言从入门到精通之【表达式和语句】

1 表达式 表达式由运算符和运算对象组成&#xff0c;最简单的表达式一个单独的运算对象。每个表达式都有一个值&#xff0c;并且是根据运算符优先级规定的顺序来执行&#xff0c;以下是一些表达式&#xff1a; 4 -6 421 a*(b c/d)/20 q 5*2 x q % 3 #q > 3 2 语句 语句…

前端web开发学习笔记

JavaWeb 前端Web开发HTMLCSSjavaScript1.JS引入2.JS基础语法3.JS函数4.JS对象 BOMDOM文档对象模型JS事件监听VueVue常用指令Vue的生命周期 AjaxAxios 前端工程化环境准备NodeJS安装和Vue-cli安装vue项目Vue组件库Element组件的使用 Vue路由Nginx打包部署 前端Web开发 HTML 负…

linux rpm安装软件卸载 以卸载mysql为例

查看rpm包 rpm -qa | grep 内容 卸载rpm rpm -e --nodeps rpm名称

使用VC++设计程序:实现常见的三种图像插值算法:最近邻插值,双线性插值,立方卷积插值

图像放大的三种插值算法 获取源工程可访问gitee可在此工程的基础上进行学习。 该工程的其他文章&#xff1a; 01- 一元熵值、二维熵值 02- 图像平移变换&#xff0c;图像缩放、图像裁剪、图像对角线镜像以及图像的旋转 03-邻域平均平滑算法、中值滤波算法、K近邻均值滤波器 04-…

VSCode 警告:v-on event ‘@toggleClick‘ must be hyphenated

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f33a; 仓库主页&#xff1a; Gitee &#x1f4ab; Github &#x1f4ab; GitCode &#x1f496; 欢迎点赞…