【C++】——list模拟实现(包懂的,细节满满)

前言

list的模拟实现和string和vector的有区别的,但是也有相同。

区别:list的迭代器底层和其他两个迭代器底层有很大区别,因为list的链式结构决定了与它们两个的不一样

相同:迭代器用法大致一样,其他成员函数的使用也大致一样。

本章不仅仅会模拟实现list,同时里面涉及的诸多细节也会一一解释,所以这次的模拟实现也可以是对之前的学习的总结和回顾。

目录

前言

list总体框架

一  list结点

二  list类框架

三  list迭代器

3.1 list迭代器框架

3.2  list常用迭代器

 3.2 list const迭代器

四  list类详解

4.1 插入和删除

 4.2  拷贝构造和赋值运算符重载

 4.3 list析构函数

五  list完整代码

六  细节处理

6.1迭代器的拷贝构造和析构函数

6.2  自定义类型

6.3  打印函数

总结


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

由于我们后面会在类外访问,所以为了避免麻烦就写成stuct,这样成员都是public

同时这里使用模板,为了适应各种数据类型

对于构造函数来说,我们直接用初始化列表进行初始化,对于_data的初始化来说,我们用const T&x=T() 这里的T()是一个匿名类型,如果我们没有传参,那么就会去调用相应类型的构造函数

二  list类框架

list类里面包含的是结点的指针,也就是哨兵位头节点的指针

template<class T>
class List
{
 public:
	typedef List_node<T> Node;//取别名
    void empty_Init()
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}
	List()//构造函数
	{
	    empty_Init();
	}
 private:
     Node*_head;//头节点
}

 由于之前封装了结点,所以这里直接用,注意因为结点的封装是一个模板,所以我们在List里面声明的时候也要带上模板,不然会显示找不到对应的构造函数的

因为是双向循环链所以一开始先把所有指针都指向自己就行(注意需要new一个结点出来,这里很容易忘记)

构造的细节并没有写在构造函数里面,而是用一个函数封装起来,因为在之后的拷贝构造中我们也需要构造一个头节点的步骤。

 

三  list迭代器

3.1 list迭代器框架

在前面说了,这里的迭代器是用封装加运算符重载来实现的,由于迭代器也会有很多类型,所以我们使用模板的形式

template<class T>
	struct _list_iterator
	{
		typedef list_node<T> Node;//我们需要一个结点指针去指向结点
		typedef _list_iterator<T> iterator;//名字太长不好写
		Node* _node;

		_list_iterator(Node* node) :_node(node)//构造函数
		{}
    }

这里也要注意,因为前面都是模板,所以记得<T>

对于这里的构造函数来说,因为我们使用迭代器都是用一个结点的指针进行构造,同时单参数的构造函数支持隐式类型的转换(如果不理解,可以往后看

3.2  list常用迭代器

list迭代器的各种操作都是用运算符重载来模拟的,所以我们可以写出下面代码

template<class T>
	struct _list_iterator
	{
		typedef list_node<T> Node;
		typedef _list_iterator<T> iterator;
		Node* _node;

		_list_iterator(Node* node) :_node(node)
		{}
		iterator& operator++()//前置
		{
			_node = _node->_next;//直接往下一个结点走
			return *this;
		}
		iterator operator++(int)//后置
		{
			iterator tem(*this);
			_node = _node->_next;
			return tem;
		}
		iterator& operator--()
		{
			_node = _node->_prve;//往前一个结点走
			return *this;
		}
		iterator operator--(int)
		{
			iterator tem(*this);
			_node = _node->_prve;
			return tem;
		}
		T& operator*()
		{
			return _node->_data;//取出结点的数据
		}
        T* operator->()
		{
			return &_node->_data;
		}
		bool operator!=(const iterator& s)
		{
			return _node != s._node;//比较结点是否是同一个
		}
	};

迭代器的每一个操作都采用了运算符重载,其实这样看来还是指针在操作,只不过他不是直接的进行操作,而是换了一种方式

 3.2 list const迭代器

这里的const迭代器也和之前的迭代器有很大不同,之前的迭代器直接在前面加const就行,如果我们直接用 const iterator,那么这个const的作用是现在迭代器的变化,而不是限制迭代器指向的内容,所以这样使用会导致迭代器无法移动

 对于之前的string来说,const迭代器是const char* ,它修饰的是指向字符串的内容,但是这里是const iterator ,它修饰的是iterator,所以两者是不一样的

 所以我们只能再封装一个const迭代器

template<class T>
	struct const_list_iterator
	{
		typedef list_node<T> Node;
		typedef const_list_iterator<T> _iterator;
		Node* _node;

		const_list_iterator(Node* node) :_node(node)
		{}
		iterator& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		iterator operator++(int)//后置
		{
			iterator tem(*this);
			_node = _node->_next;
			return tem;
		}
		iterator& operator--()
		{
			_node = _node->_prve;
			return *this;
		}
		iterator operator--(int)
		{
			iterator tem(*this);
			_node = _node->_prve;
			return tem;
		}
		const T& operator*()const 
		{
			return _node->_data;
		}
		const T* operator->()const
		{
			return &_node->_data;
		}
		bool operator!=(const iterator& s)
		{
			return _node != s._node;
		}
	};

但是我们发现const迭代器和非const迭代器的区别就是在 

这两个运算符重载的区别就在于这两个,变化很小,但是我们写了两个迭代器,这样看起来就很冗余

所以这里也可以采用模板的形式对它进行改造。

思路就是先写一个迭代器模板,用这个模板去构造两种迭代器,把任务交给编译器就行

template<class T,class Ref,class Ptr>
struct _List_iterator
{
	typedef _List_iterator<T,Ref,Ptr> iterator;
	typedef List_node<T> Node;
	Node* _node;
	//构造函数
	_List_iterator(Node* node):_node(node)
	{}
	iterator& operator++()
	{
		_node = _node->_next;
		return *this;
	}
	iterator& operator++(int)
	{
		iterator tmp(*this);
		_node = _node->_next;
		return tmp;
	}
	iterator& operator--()
	{
		_node = _node->_prev;
		return *this;
	}
	iterator& operator--(int)
	{
		iterator tmp(*this);
		_node = _node->_prev;
		return tmp;
	}
	 Ref operator*()
	 {
		return _node->_data;
	 }
	 Ptr operator->()
	 {
		 return &_node->_data;
	 }
	bool operator!=(const iterator& s)
	{
		return _node != s._node;
	}

};

由于需要改动的重载函数,有两个不同的类型一个是*,一个是&,所以模板里面再加两个参数

有了这个模板,我们再list里面可以去构造两种迭代器 。

四  list类详解

弄好了迭代器,我们就可以写相应的成员函数了。

4.1 插入和删除

对于插入和删除来说,就是数据结构中链表的插入和删除

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

		newnode->_next = cur;
		cur->_prev = newnode;
		return newnode;//这里不是pos的位置改变,这里返回的是新插入的位置
                       //在用的时候可以不去接收返回值
	}
	iterator erase(iterator pos)
	{
		Node* cur = pos._node;
		Node* next = cur->_next;
		Node* prev = cur->_prev;

		delete cur;
		prev->_next = next;
		next->_prev = prev;
		return next;//迭代器会失效
	}

这里的插入位置都是迭代器类型的变量

之前vector的插入和删除操作会导致迭代器失效的问题,list的插入操作不会导致迭代器失效,因为list没有扩容的操作。所以也就没有改变相应的位置,但是删除操作有迭代器失效的问题,所以我们在使用的时候记得去接收一下新的位置 

有了插入和删除这两个函数,那我们就可以轻松实现头插,尾插,头删,尾删了

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());
	}
 4.2  拷贝构造和赋值运算符重载

对于这两个,相信我们都不会陌生了

拷贝构造:一个已经初始化的对象和一个没有初始化的对象

赋值运算符重载:两个都是已经存在的对象,所以赋值运算符重载另外一种写法就是先清理数据然后再一个个尾插。

拷贝构造

List(const List<T>& lt)
	{
		empty_Init();
		for (auto e : lt)
		{
			push_back(e);
		}
	}

 赋值运算符重载

	void swap( List<T>tmp)
	{
		std::swap(_head, tmp._head);
	}
	List<T>operator=( List<T> tmp)
	{
		swap(tmp);
		return *this;
	}

这里再对赋值运算符重载进行一下说明,我们在用的时候会传参给tmp,这里调用拷贝构造,创建了一个新的结点,所以交换一下,并没有说两个对象指向同一块空间,所以不存在析构两次的问题

对于赋值运算符重载来说,还有一种写法,就是先清理数据,然后再一个个尾插

 4.3 list析构函数
~List()
	{
		iterator it =begin();
		while(it != end())
		{
			it=erase(it);
		}
	}

就是把数据一个个删除就行。

 

五  list完整代码

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

template<class T,class Ref,class Ptr>
struct _List_iterator
{
	typedef _List_iterator<T,Ref,Ptr> iterator;
	typedef List_node<T> Node;
	Node* _node;
	//构造函数
	_List_iterator(Node* node):_node(node)
	{}
	iterator& operator++()
	{
		_node = _node->_next;
		return *this;
	}
	iterator& operator++(int)
	{
		iterator tmp(*this);
		_node = _node->_next;
		return tmp;
	}
	iterator& operator--()
	{
		_node = _node->_prev;
		return *this;
	}
	iterator& operator--(int)
	{
		iterator tmp(*this);
		_node = _node->_prev;
		return tmp;
	}
	 Ref operator*()
	 {
		return _node->_data;
	 }
	 Ptr operator->()
	 {
		 return &_node->_data;
	 }
	bool operator!=(const iterator& s)
	{
		return _node != s._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 _head->_next;
	}
	iterator end()
	{
		return _head;
	}
	const_iterator begin()const
	{
		return _head->_next;
	}
	const_iterator end()const
	{
		return _head;
	}
	//构造函数
	void empty_Init()
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}
	List()
	{
	    empty_Init();
	}
	~List()
	{
		iterator it =begin();
		while(it != end())
		{
			it=erase(it);
		}
	}
	//
	List(const List<T>& lt)
	{
		empty_Init();
		for (auto e : lt)
		{
			push_back(e);
		}
	}
	void swap( List<T>tmp)
	{
		std::swap(_head, tmp._head);
	}
	List<T>operator=( List<T> tmp)
	{
		swap(tmp);
		return *this;
	}
	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());
	}
	
	iterator insert(iterator pos, const T& x)
	{
		Node* cur = pos._node;
		Node*prve = cur->_prev;
		Node* newnode = new Node(x);
		prve->_next = newnode;
		newnode->_prev = prve;

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

		delete cur;
		prev->_next = next;
		next->_prev = prev;
		return next;
	}
	
private:
	Node* _head;
};

//打印
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;
}

六  细节处理

6.1迭代器的拷贝构造和析构函数

在上面代码中我们没有写迭代器的析构函数和拷贝构造,因为我们不需要析构函数,我们不能说遍历一遍结点就把结点给释放了,再者就是拷贝构造

    List<int>::iterator it = lt.begin();这断代码表示的是lt.begin()返回一个迭代器类型,然后拷贝构造给it,那存不存在析构两次的问题呢,答案是不存在

我们需要的就是浅拷贝,我们要的就是it指向这个结点的位置,同时也只有它指向,所以在函数结束的时候,这个it会被销毁。

所以我们不需要写拷贝构造和析构函数,拷贝构造直接用编译器的浅拷贝就行。对于析构函数来说,因为这里的迭代器只起到遍历的作用,所以不需要去释放任何资源。而且如果写了析构还会出现各种析构两次的问题

6.2  自定义类型
class AA
{
public:
	AA(int a1=0,int a2=0):_a1(a1),_a2(a2)
	{}

	int _a1;
	int _a2;
};
void test1()
{
	List<AA>lt;
	lt.push_back(AA(1, 1));
	lt.push_back(AA(2, 2));
	lt.push_back(AA(2, 2));
	List<AA>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << *it << " ";
		it++;
	}
}

对于这个来说这样写是错误的,因为AA类里面没有重载<<这个运算符

所以我们得换一直方式

void test1()
{
	List<AA>lt;
	lt.push_back(AA(1, 1));
	lt.push_back(AA(2, 2));
	lt.push_back(AA(2, 2));
	List<AA>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << (*it)._a1 << " " << (*it)._a2 << endl;
		it++;
	}
	
	while (it != lt.end())
	{
		cout << it->_a1 << " " << it->_a2 << endl;
		it++;
	}
}

这就是为什么要重载->运算符的原因,这个是给自定义类型用的,对于知内置类型直接解引用就行。

6.3  打印函数

在上面的完整代码中有一个打印函数,它也是用模板实现的,主要是为了打印各种容器

//打印
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;
}

 这里的不同是我们之前都是用class的,这里用的是typename,原因在于编译器在编译的时候,会去Container里面去找const_iterator这个类型,但是Container是一个模板,并没有实例化,所以编译器也不知道怎么定义,所以就在编译的时候就过不了,但是我们在前面加一个typename就会告诉编译器,先过,后面再来实例化,这样就可以解决问题了

总结

以上就是list模拟实现的全部内容了,希望对你有所帮助 

 

 

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

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

相关文章

解决Mac ~/.bash_profile 配置的环境变量重启终端后失效问题

在Mac系统中&#xff0c;配置环境变量通常是在~/.bash_profile文件中进行。然而&#xff0c;有时会遇到配置的环境变量在重启终端后失效的问题。 解决办法&#xff1a; 在~/.zshrc文件最后或最前面&#xff0c;增加一行 source ~/.bash_profile

Linux 搭建 ZeroTier 的 Moon 服务器

系统&#xff1a;centos 7.6 轻量云服务器&#xff1a;腾讯云 Moon是什么&#xff0c;为什么需要Moon&#xff1f; ZeroTier通过自己的多个根服务器帮助我们建立虚拟的局域网&#xff0c;让虚拟局域网内的各台设备可以打洞直连。这些根服务器的功能有些类似于通过域名查询找到…

SOFA-RPC学习记录

文章目录 需求分析模块划分微服务模块交互模块 可拓展架构插件机制 功能分析交互模块 学习微服务模块交互模块 dubbo与nacos集成学习Nacos配置中心实战 dubbo与apollo集成学习配置中心组件与k8s的抉择参考资料 结论 本报告旨在深入学习SOFA-RPC框架&#xff0c;特别是其动态配置…

基于小波区间相关的信号降噪方法(MATLAB 2021B)

在我们处理信号过程中最重要的任务就是找到信号隐藏的规律和信号的特征。常用的傅里叶分析法只能用于在时间域或者频率域上分析信号&#xff0c;而通常的数据会在时间域和频率域均有特征。而小波分析是继傅里叶分析之后的一大突破性创新&#xff0c;也是近年来在学术界非常热门…

python字符串的索引

上一篇回顾 上一关中&#xff0c;我们重识了 字符串&#xff0c;还了解了 字符串拼接 和 字符串格式化输出 的方法。 字符串的“乘法”可以很方便地“复读”字符串&#xff0c;让字符串与一个整数 n 相乘&#xff0c;字符串就会原样复制 n 次。 在体验课中我们学到&#xff…

嵌入式Linux系统编程 — 1.2 文件管理与错误处理

目录 1 Linux 系统如何管理文件 1.1 什么是静态文件 1.2 扇区&#xff08;Sector&#xff09;和块&#xff08;Block&#xff09;概念&#xff1f; 1.3 inode 1.4 进程控制块&#xff08;PCB&#xff09; 2 返回错误处理与 errno 2.1 errno变量介绍 2.3 perror函数介绍…

python-验证子串

题目描述 输入两个字符串&#xff0c;验证其中一个串是否为另一个串的子串。 输入两个字符串&#xff0c; 每个字符串占一行&#xff0c;长度不超过200且不含空格。 输出 若第一个串s1是第二个串s2的子串&#xff0c;则输出(s1) is substring of(s2)否则&#xff0c;若第二个串…

【云原生】kubernetes中secret原理详解与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

LayUI的暗淡:错误的押宝了前后端不分离

LayUI是一个不错的中后台UI框架&#xff0c;贝格前端工场用的CMS就是基于layUI的&#xff0c;可以说简单轻便。除此之外&#xff0c;贝格前端工场很少接到客户要求升级LayUI界面&#xff0c;或者采用LayUI框架的。 一、LayUI官网的谢幕&#xff0c;吹起了前后端不分离模式没落…

拓扑排序详解

目录 一、拓扑排序前置知识 1.1 定义&#xff1a; 1.2 AOV网&#xff1a; 二、如何拓扑排序 2.1 运用 kahn 算法&#xff1a; 2.2 实现拓扑排序&#xff1a; 2.3 拓扑排序的应用&#xff1a; 2.4 拓扑排序编写模板&#xff1a; 三、例题练习 3.1 例题1&#xff1a;课…

浙江大爱遮阳新材料股份有限公司新品发布会圆满成功

5月29日,浙江大爱遮阳新材料股份有限公司新品发布会在上海国家会展中心举办。本次会议出席的嘉宾有浙江大爱遮阳新材料股份有限公司总经理俞彬军,常务副总王志华,上海大爱益可美遮阳科技有限公司总经理陆俊青,浙江大爱遮阳新材料股份有限公司销售经理平鸿烈,销售经理蒋扬锋和玛…

【Python】轻松打包:CentOS7上使用PyInstaller将Shell脚本转换为可执行文件的完美指南

【Python】轻松打包&#xff1a;CentOS7上使用PyInstaller将Shell脚本转换为可执行文件的完美指南 大家好 我是寸铁&#x1f44a; 总结了一篇【Python】轻松打包&#xff1a;CentOS7上使用PyInstaller将Shell脚本转换为可执行文件的完美指南✨ 喜欢的小伙伴可以点点关注 &#…

C# 生成解决方案时出现的一些异常及解决方法

一、ResolveAssemblyReference任务意外失败 在使用VS2022生成C#解决方案时&#xff0c;出现如下错误&#xff1a; 解决方法&#xff1a; 项目的依赖项出现问题&#xff0c;重新更新一下依赖项即可 二、生成Win32资源时出错 产生这个原因的主要原因是配置的应用程序的图标文…

Thesios: Synthesizing Accurate Counterfactual I/O Traces from I/O Samples——论文泛读

ASPLOS 2024 Paper 论文阅读笔记整理 问题 在设计大规模分布式存储系统时&#xff0c;I/O活动的建模至关重要。具有代表性的/O跟踪&#xff0c;可以对现有硬件、配置和策略进行详细的性能评估。假设跟踪进一步支持分析假设情况&#xff0c;例如部署新的存储硬件、更改配置和修…

【机器学习】机器学习在深度学习领域中的作用:半监督学习的视角

&#x1f440;时空之门&#x1f440; &#x1f50d;引言&#x1f388;半监督学习概述&#x1f69d;机器学习在深度学习领域中的作用☘特征提取与表示学习&#x1f340;复杂任务建模❀结合半监督学习提升性能 &#x1f680;半监督学习在深度学习中的应用场景&#x1f4d5;图像识…

使用import语句导入模块

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 创建模块后&#xff0c;就可以在其他程序中使用该模块了。要使用模块需要先以模块的形式加载模块中的代码&#xff0c;这可以使用import语句实现。im…

智能体应用开发:构建各类垂直领域的ai智能体应用

最近在做个类似的项目&#xff0c;有用到这方面的知识&#xff0c;顺便做一些记录和笔记吧&#xff0c;希望能帮到大家了解智能体应用开发 目录 引言 AI原生应用的兴起 智能体在AI中的角色 实现原理详解 机器学习基础 数据管理与关联数据库 数据结构 Embedding 检索方…

CSS - 元素竖向百分比的基准值是什么?

例如有一个外层DIV元素&#xff0c;设定width为500px&#xff0c;height为300px。然后在其内部添加一个DIV元素&#xff0c;这个时候&#xff0c;内部的DIV元素&#xff0c;如果设定height margin-top padding-top 百分比之后&#xff0c;他们的百分比基准值是什么呢&#xff1…

基于JSP的母婴用品网站系统

你好呀&#xff0c;我是计算机学长猫哥&#xff01;如果有需求可以文末加我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSP技术 工具&#xff1a;IDEA/Eclipse、Navicat、Maven 系统展示 首页 管理员功能界面 用户功能界面 前台首页功能界面 …

2024-6-4 石群电路-23

2024-6-4&#xff0c;星期二&#xff0c;13:16&#xff0c;天气&#xff1a;晴&#xff0c;心情&#xff1a;晴。今天又是阳光明媚的一天&#xff0c;没有什么特别的事情发生&#xff0c;加油学习喽~ 今日观看了石群老师电路课程的第39和第40个视频&#xff0c;继续第九章的学…