【CPP】slt-list由认识到简化模拟实现深度理解~

news2024/11/15 15:35:08

关于我:在这里插入图片描述


睡觉待开机:个人主页

个人专栏: 《优选算法》《C语言》《CPP》
生活的理想,就是为了理想的生活!
作者留言

PDF版免费提供倘若有需要,想拿我写的博客进行学习和交流,可以私信我将免费提供PDF版。
留下你的建议倘若你发现本文中的内容和配图有任何错误或改进建议,请直接评论或者私信。
倡导提问与交流关于本文任何不明之处,请及时评论和私信,看到即回复。


参考目录

  • 1.前言
  • 2.list简介
  • 3.list重点接口
    • 3.1构造函数constructor
    • 3.2operator=
    • 3.3max_size
    • 3.4assigne重写
    • 3.5insert
    • 3.6splice转移
    • 3.7remove
    • 3.8unique去重
    • 3.9merge合并
    • 3.10sort排序
  • 4.快速模拟最基本的list
    • 4.1自定义命名空间域和list成员变量
    • 4.2node的自定义
    • 4.3list::list()无参构造
    • 4.4list::push_back()
    • 4.5list::迭代器的设计
    • 4.6小总结
  • 5.进一步完善list
    • 5.1list的insert函数接口实现
    • 5.2list::size()
    • 5.3list::erase()
    • 5.4pop_back,pop_front,push_back,push_front
  • 6.完善迭代器
    • 6.1operator->的理解
    • 6.2const迭代器
  • 7.析构、拷贝构造、赋值运算符重载
    • 7.1析构函数
    • 7.2拷贝构造
    • 7.3赋值运算符重载

1.前言

今天简单分享一下STL中的一大容器——List
先介绍后模拟
在这里我会先以CPP文档为样去介绍其中比较重点的函数接口,之后我会简单的去模拟实现并详细介绍为什么去这样写,期间会配有一些图片和说文便于读者理解。
但请注意模拟实现的list是一个尽可能对STL源码进行精简的版本,可以说存在相当大的缺陷和漏洞,但我想对于了解list基本底层而言,这是足够的。


2.list简介

文档链接:LIST

std::list

template < class T, class Alloc = allocator > class list;

list是一个带头双向循环链表并且允许O(1)时间复杂度内插入或者删除一个结点。
在这里插入图片描述
我们该怎么理解带头双向循环链表呢?
带头:指的是带哨兵位结点的链表,这个哨兵位仅仅是用来方便对链表操作而额外申请的空间,并不表示有效数据。
双向:指的是链表的指向方向是双向的,也就是对于指向一个结点的指针,既可以向前走,找到他的前一个结点,也可以向后走,找到它的后一个结点。
循环:循环,就是链表可以抽象为一个环,即首尾相连,可以理解为哨兵位的前一个结点指向最后一个结点,最后一个结点的后一个结点是哨兵位。
链表:是一种数据结构,线性表的一种,是逻辑结构是线性但是物理逻辑不是线性的一种线性表。具体是怎么样的,我想可以参考后文中的链表抽象图。
在这里插入图片描述

好的,简单了解了带头双向链表的意思,我们简单介绍一下相关接口的使用。


3.list重点接口

重点函数,测试功能代码
在这一段落中,我并非将每个函数面面俱到去说的很详细,也不会很全面,我只提及我认为比较容易出错或者重要也或者说是常用的函数接口,同时给出测试代码来验证这个函数的相关功能。

3.1构造函数constructor

在这里插入图片描述

//无参构造
list<int> l1;
//n个val构造
list<int> l2(10, 1);
//迭代器构造
list<int> l3(l2.begin(), l2.end());
//拷贝构造
list<int> l4(l3);

for (auto& node : l1)
{
    cout << node << " ";
}
cout << endl;
for (auto& node : l2)
{
    cout << node << " ";
}
cout << endl;
for (auto& node : l3)
{
    cout << node << " ";
}
cout << endl;
for (auto& node : l4)
{
    cout << node << " ";
}
cout << endl;

3.2operator=

在这里插入图片描述

list<int> l1;
list<int> l2(10, 6);
l1 = l2;
for (auto& node : l1)
{
	cout << node << " ";
}
cout << endl;
for (auto& node : l2)
{
	cout << node << " ";
}

3.3max_size

在这里插入图片描述

list<int> l;
cout << l.max_size() << endl;

//结果:768614336404564650

3.4assigne重写

在这里插入图片描述

//assign
list<int> al(5, 6);
list<int> l(10, 1);
//支持迭代器重写
l.assign(al.begin(), al.end());
for (auto& node : l)
{
	cout << node << " ";
}
cout << endl;
//也支持n个val值重写
l.assign(12, 3);
for (auto& node : l)
{
	cout << node << " ";
}
cout << endl;

3.5insert

在这里插入图片描述

//insert
list<int> l(3, 3);
for (auto& node : l)
{
    cout << node << " ";
}//3 3 3
cout << endl;
//支持单个值插入
l.insert(l.begin(), 0);
for (auto& node : l)
{
    cout << node << " ";
}//0 3 3 3
cout << endl;
//支持插入n个val值插入
l.insert(l.end(), 4, 4);
for (auto& node : l)
{
    cout << node << " ";
}//0 3 3 3 4 4 4 4
cout << endl;
//支持迭代器插入一段区间
list<int> cl(6, 6);
l.insert(l.end(), cl.begin(), cl.end());
l.insert(l.end(), 4, 4);
for (auto& node : l)
{
    cout << node << " ";
}//0 3 3 3 4 4 4 4 6 6 6 6 6 6 4 4 4 4
cout << endl;

问题:我们vector中insert会引发迭代器失效问题,我们list中的insert会有迭代器失效问题吗?为什么?
答:不会。这是由底层结构决定的。

在这里插入图片描述
同理,erase也会引起vector失效,但是对于list就不会引起迭代器失效问题。

3.6splice转移

在这里插入图片描述

//splice 转移
//把一个链表转移到另一个链表
list<int> l;
list<int> l2(10, 6);
l.splice(l.begin(), l2);
cout << 'l' << ":" << endl;
for (auto& node : l)
{
    cout << node << " ";
}
cout << endl;
cout << "l2" << ":" << endl;
for (auto& node : l2)
{
    cout << node << " ";
}
cout << endl;
//把一个链表的一个元素转移给另一个链表
list<int> l;
list<int> l2(10, 6);
l.splice(l.begin(), l2, l2.begin());
cout << 'l' << ":" << endl;
for (auto& node : l)
{
	cout << node << " ";
}
cout << endl;
cout << "l2" << ":" << endl;
for (auto& node : l2)
{
	cout << node << " ";
}
cout << endl;
//把一个链表的一部分转移给另一个链表
list<int> l;
list<int> l2(10, 6);
l.splice(l.begin(), l2, ++l2.begin(),l2.end());
cout << 'l' << ":" << endl;
for (auto& node : l)
{
	cout << node << " ";
}
cout << endl;
cout << "l2" << ":" << endl;
for (auto& node : l2)
{
	cout << node << " ";
}
cout << endl;

3.7remove

在这里插入图片描述

list<int> l(10, 1);
l.remove(1);
for (auto& node : l)
{
	cout << node << " ";
}//输出为空
cout << endl;

3.8unique去重

在这里插入图片描述

//list未排序情况下使用去重unique
list<int> l;
l.push_back(1);
l.push_back(3);
l.push_back(3);
l.push_back(2);
l.push_back(3);
l.push_back(4);
l.unique();
for (auto& node : l)
{
	cout << node << " ";
}//1 3 2 3 4
cout << endl;
//list排序情况下使用去重unique
list<int> l;
l.push_back(1);
l.push_back(3);
l.push_back(3);
l.push_back(2);
l.push_back(3);
l.push_back(4);
l.sort();
l.unique();
for (auto& node : l)
{
	cout << node << " ";
}//1 2 3 4
cout << endl;

3.9merge合并

在这里插入图片描述

//未排序使用merge
list<int> l1;
l1.push_back(1);
l1.push_back(3);
l1.push_back(4);
l1.push_back(1);
list<int> l2;
l2.push_back(1);
l2.push_back(3);
l2.push_back(4);
l2.push_back(1);
l1.merge(l2);//assert断言错误
for (auto& node : l1)
{
	cout << node << " ";
}
cout << endl;
for (auto& node : l2)
{
	cout << node << " ";
}
cout << endl;
//未排序使用merge
list<int> l1;
l1.push_back(1);
l1.push_back(3);
l1.push_back(4);
l1.push_back(1);
l1.sort();
list<int> l2;
l2.push_back(1);
l2.push_back(3);
l2.push_back(4);
l2.push_back(1);
l2.sort();
l1.merge(l2);//assert断言错误
for (auto& node : l1)
{
	cout << node << " ";
}//1 1 1 1 3 3 4 4
cout << endl;
for (auto& node : l2)
{
	cout << node << " ";
}//空
cout << endl;
//未排序使用merge
list<int> l1;
l1.push_back(1);
l1.push_back(3);
l1.push_back(4);
l1.push_back(1);
l1.sort(greater<int>());
list<int> l2;
l2.push_back(1);
l2.push_back(3);
l2.push_back(4);
l2.push_back(1);
l2.sort(greater<int>());
l1.merge(l2, greater<int>());//com函数必须提供由sort的一致,不然断言错误
for (auto& node : l1)
{
	cout << node << " ";
}//4 4 3 3 1 1 1 1
cout << endl;
for (auto& node : l2)
{
	cout << node << " ";
}//空
cout << endl;

3.10sort排序

排序效率一般:这里需要重点强调一下list中的sort排序和vector中的sort排序效率差距还是挺大的,建议数据量比较大的话有条件就用vector进行排序,即使是从list把数据拷贝到vector再拷贝回list
list提供了自己的sort排序接口:
在这里插入图片描述

//sort
list<int> l;
l.push_back(1);
l.push_back(3);
l.push_back(2);
l.push_back(5);
l.push_back(4);
l.sort();
for (auto& node : l)
{
	cout << node << " ";
}//1 2 3 4 5
cout << endl;
void test_op1()
{
	srand(time(0));
	const int N = 1000000;//一百万数据

	//两个链表
	list<int> lt1;
	list<int> lt2;

	//一个顺序表
	vector<int> v;

	//生成随机数据,尾插到链表1和顺序表v中去
	for (int i = 0; i < N; ++i)
	{
		auto e = rand() + i;//加上这个i主要是为了减少重复数字概率
		lt1.push_back(e);
		v.push_back(e);
	}

	//vector排序
	int begin1 = clock();
	sort(v.begin(), v.end());
	int end1 = clock();

	//list排序
	int begin2 = clock();
	lt1.sort();
	int end2 = clock();

	//打印比较两者用时
	printf("vector sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
	//vector sort : 253
	//list sort : 411
}
void test_op2()
{
	srand(time(0));//种子
	const int count = 1000000;
	
	//两个链表
	list<int> l1;
	list<int> l2;

	//制造数据并加入到l1和l2中
	for (int i = 0; i < count; i++)
	{
		int n = rand();
		l1.push_back(n);
		l2.push_back(n);
	}

	//l1自己排序
	size_t begin1 = clock();
	l1.sort();
	size_t end1 = clock();
	//让l2先把数据拷贝到v中,再在v中进行排序,之后拷贝回l2中
	size_t begin2 = clock();
	vector<int> v(l2.begin(), l2.end());
	sort(v.begin(), v.end());
	l1.insert(l1.begin(), v.begin(), v.end());
	size_t end2 = clock();
	
	cout << end1 - begin1 << endl;//755
	cout << end2 - begin2 << endl;//520
}

结论:数据量比较大的时候尽量少用list进行排序。

4.快速模拟最基本的list

我们去模拟一个东西,应该先去搞出这个东西的大致框架,然后再去逐渐完善,在这里我就很简单的写一个list模拟实现。
请注意,这里仅供参考,list都有不同的实现方式,且本模拟实现仅是个框架。

我们知道写代码的过程往往十分复杂,即使是一个几百行的简单项目,用图文的形式呈现出来更是需要大量的篇幅。基于此,我将不再叙说我写的时候走过的弯路,直接是用一气呵成的顺序,按照逻辑一步一步呈现相关内容。

4.1自定义命名空间域和list成员变量

我们知道,模拟写链表类最好先写一个自定义命名空间域,这样不容易与其他写的代码相冲突。
在这里插入图片描述

同时,我们想链表中最重要的肯定就是相关的成员变量,那链表中需要用到的只有一个,即头节点指针

namespace szg
{
template<class T>
class list
{
private:
		node* _head;
}
}

但是这个头节点指针是CPP本身就有的吗?显然不是,所以我们要去写一个node的自定义类型:

4.2node的自定义

template<class T>
struct ListNode
{
	T _val;
	struct ListNode* _prev;
	struct ListNode* _next;

	//ListNode的构造函数
	ListNode(const T& x = T())
		:_next(nullptr)
		, _prev(nullptr)
		, _val(x)
	{}
};

在上面struct ListNode的类,就是我写的关于node的自定义类型及其相关构造函数,这里请思考一下:
ListNode的成员变量为什么是prev和next两个指针呢? 因为我们要写的是双向链表,对于每个结点而言都需要去存储前一个结点的地址和后一个结点的地址,结点本身还要存储上自己的数据val这样才可以。
在这里插入图片描述

ListNode的构造函数为什么这样写? 因为函数头参数给缺省值是更加方便构建结点,同时对于一个新节点来说,前后指针都应指向空,方便后续操作,而val则应该放上对应的数值。

好,处理完node结点问题,然后我们继续来写我们的链表。
写好了ListNode类,我们得把他用起来啊,但是我们的list类中写的是node!=ListNode,这里只需要typedef一下就行了:

namespace szg
{
template<class T>
class list
{
private:
	typedef ListNode<T> node;
	node* _head;
}
}

在这个地方,我想补充一个知识点:
CPP中类中内嵌类型。 什么是类中内嵌类型呢?就是在一个类的内部可以直接调用的类型,特指自定义类型,比如我们上面说的ListNode类就是list的一个内嵌类型。
一般而言,定义类中内嵌类型的方法有两个,一个是在类外定义的自定义类,在要使用的类中typedef一下就行,还有一种方法就是直接定义内部类的方法。
习惯上,我们CPP更加习惯于前者,后者是JAVA常用的手段。

写好了上面代码,我们继续来写list的无参构造函数。

4.3list::list()无参构造

我是这样写的:
在list中:

//无参构造函数
list()
{
	empty_initialization();
}

void empty_initialization()
{
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
}

这里调用了一次空初始化函数哈,因为STL库中直接嵌套了四五层,这里就是模拟一下样子就好。
empty_initialization()中,我们肯定是得先new一个node空间,因为无参构造出的链表只有一个哨兵结点,所以让哨兵位的next指向他自己,哨兵位的prev也指向他自己就好了。
在这里插入图片描述

4.4list::push_back()

我们得实现一个插入结点的接口才行,所以这里我们实现最简单的尾插接口:
代码如下:

//push_back
void push_back(const T& x)
{
	//开空间
	node* newnode = new node(x);
	//编辑空间
	node* tail = _head->_prev;

	tail->_next = newnode;
	newnode->_prev = tail;
	newnode->_next = _head;
	_head->_prev = newnode;
	_size++;
}

我们想要新插入一个结点,

  • 函数参数:因为我不知道你要插入结点的val值是多少,所以你得给到我,设计上参数const T& x用来接收val数据
  • 函数体:逻辑肯定是先开空间,再把这个结点融合到链表中。
    • new一个空间,取名为newnode
    • 再把newnode融合到链表中,
      • 让原先链表的最后一个结点的后继指针指向newnode
      • 再让newnode的前驱指针指向原先链表的最后一个结点
      • newnode的后继指针指向哨兵位
      • 哨兵位的前驱指针指向newnode

好的,这样整体上就完成了push_back()的逻辑。
在这里插入图片描述
然后我们稍微测试一下我们写的是否正确,当然需要打印出来,因此需要写一个迭代器去访问测试一下。

4.5list::迭代器的设计

但是这里有个问题:迭代器怎么写???用原生指针typedef一下吗?
当然不行。
在vector中,用原生指针充当迭代器是完全可以的,但是对于list,原生指针++或者–操作之后,会指向内存的下一块区域,问题就是list在内存中实际的存储是不确定的。
在这里插入图片描述
重要的原因在于,这个原生指针++、–操作之后的行为不是我们想要的,如果我们可以修改他的++、–行为岂不是很好吗?
于是,我们将原生指针封装为一个类,重载他的运算符,就解决了这个问题。本质上,封装原生指针就是为了扩大我们对指针的控制权限。

思考之后,我写出了下面的代码:

template<class T>
struct ListIterator
{
public:
	typedef ListNode<T> node;
	node* _iterator;
}

好,我们开始重载他的运算符。
不过在这之前,我们得先把这个迭代器的构造函数写一下。

//itrator构造
ListIterator(node* node)
	:_iterator(node)
{}

之后我们重载他的相关运算符:包括解引用、加加、减减、等于和不等于。

public:
	node* GetIterator()
	{
		return _iterator;
	}
	node* GetIterator() const
	{
		return _iterator;
	}
//解引用重载
T& operator* ()
{
	return _iterator->_val;
}

//前置++重载
ListIterator<T>& operator++()
{
	_iterator = _iterator->_next;

	return *this;
}
//后置++重载
ListIterator<T> operator++(int)
{
	ListIterator<T> temp(*this); // 构建一个临时对象
	_iterator = _iterator->_next; // 让当前this指向下一个结点

	return temp; // 但是返回临时变量
}

//!=重载
bool operator!=(const ListIterator<T>& l)
{
	return this->_iterator != l.GetIterator();
}
//==重载
bool operator==(const ListIterator<T>& l)
{
	return this->_iterator == l.GetIterator();
}

为了便于大家理解,我在这里提出下面几个问题帮助大家进一步理解上面代码:

  • 为什么list的迭代器需要对原生指针进行封装?答:为了重载他的操作符
  • 迭代器需要写构造函数和拷贝构造函数吗?为什么?构造函数需要写,拷贝构造不用谢,因为编译器自动浅拷贝。
  • 迭代器需要写析构函数吗?不用,因为迭代器不用考虑结点的释放,释放结点属于list的工作
  • 迭代器什么操作符需要进行重载?根据需要和实际意义,比如在当前场景下迭代器重载大于和小于就没有什么实际意义,可以选择不重载。
  • 上面我们写的iterator类准确来说是类型还是迭代器?是迭代器类型,迭代器是在list中实现的。

好的,相必大家已经了解了上面所说的要点,那我们回到list并为list写上一个简单的迭代器。

template<class T>
class list
{
private:
	typedef ListNode<T> node;
	typedef ListIterator<T> iterator;
	node* _head;
public:
	//无参构造函数
	void empty_initialization()
	{
		_head = new node;
		_head->_next = _head;
		_head->_prev = _head;
	}
	list()
	{
		empty_initialization();
	}

	//基本迭代器
	iterator begin()//迭代器怎么不返回引用呢?begin()默认指向第一个有效节点
	{
		return iterator(_head->_next);//返回匿名对象
	}
	iterator end()
	{
		return iterator(_head);//返回匿名对象
	}

	//push_back
	void push_back(const T& x)
	{
		//开空间
		node* newnode = new node(x);
		//编辑空间
		node* tail = _head->_prev;

		tail->_next = newnode;
		newnode->_prev = tail;
		newnode->_next = _head;
		_head->_prev = newnode;
		_size++;
	}
};

上面代码中的begin和end函数才是我们说的迭代器。

请思考一下为什么这个begin和end的返回类型是iterator,而不能是iterator&,为什么是迭代器值返回,而不是引用返回呢,毕竟引用返回效率更高啊?
因为迭代器如果返回引用,就会造成很大的问题,毕竟外界可以修改begin和end,这也就会造成begin/end的指向错误。
我用下面例子来进行说明:
在这里插入图片描述
在这里插入图片描述

至此,我们写出了下面代码:

4.6小总结

#include<iostream>

namespace szg
{
	template<class T>
	struct ListNode
	{
		T _val;
		struct ListNode* _prev;
		struct ListNode* _next;

		//ListNode的构造函数
		ListNode(const T& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _val(x)
		{}
	};

	template<class T>
	struct ListIterator
	{
	public:
		typedef ListNode<T> node;
		node* _iterator;
	public:
		//itrator构造
		ListIterator(node* node)
			:_iterator(node)
		{}

		//解引用重载
		T& operator* ()
		{
			return _iterator->_val;
		}

		//前置++重载
		ListIterator<T>& operator++()
		{
			_iterator = _iterator->_next;

			return *this;
		}
		//后置++重载
		ListIterator<T> operator++(int)
		{
			ListIterator<T> temp(*this); // 构建一个临时对象
			_iterator = _iterator->_next; // 让当前this指向下一个结点

			return temp; // 但是返回临时变量
		}

		//!=重载
		bool operator!=(const ListIterator<T>& l)
		{
			return this->_iterator != l._iterator;
		}
		//==重载
		bool operator==(const ListIterator<T>& l)
		{
			return this->_iterator == l._iterator;
		}
	};

	template<class T>
	class list
	{
	private:
		typedef ListNode<T> node;
		typedef ListIterator<T> iterator;
		node* _head;
		size_t _size;
	public:
		//无参构造函数
		void empty_initialization()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;

			_size = 0;
		}
		list()
		{
			empty_initialization();
		}

		//基本迭代器
		iterator begin()//迭代器怎么不返回引用呢?begin()默认指向第一个有效节点
		{
			return iterator(_head->_next);//返回匿名对象
		}
		iterator end()
		{
			return iterator(_head);//返回匿名对象
		}

		//push_back
		void push_back(const T& x)
		{
			//开空间
			node* newnode = new node(x);
			//编辑空间
			node* tail = _head->_prev;

			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
			_size++;
		}
	};
}

我们不妨来测试一下是否能够正常运行>>

//test.cpp
#include"List.h"

int main()
{
	szg::list<int> l;
	l.push_back(1);
	l.push_back(2);
	l.push_back(3);
	l.push_back(4);

	//访问
	szg::ListIterator<int> it = l.begin();
	while (it != l.end())
	{
		std::cout << *it << " ";
		it++;
	}
	std::cout << std::endl;

	return 0;
}

输出结果:
在这里插入图片描述

5.进一步完善list

这一部分内容很简单,没有什么难理解的地方,无非就是大量复用已经实现的接口。在这里我就不再依次进行详述。

5.1list的insert函数接口实现

我们结合CPP文档中insert的功能,我们需要先传入一个迭代器指定位置,之后传入要插入的x数值。
经过思考,我们可以这样实现代码:

//inser
void insert(iterator pos, const T& x)
{
	// 开空间
	node* newnode = new node(x);

	// 记录一下位置
	node* prev = pos._iterator->_prev;
	node* pcur = pos._iterator;

	// 组织逻辑
	newnode->_next = pcur;
	pcur->_prev = newnode;
	newnode->_prev = prev;
	prev->_next = newnode;
}

思考:这个地方为啥是node* prev;node* pcur???不能用iterator类型吗?
答:也可以写成iterator,只不过写成node*后面组织逻辑得时候更加方便而已。

5.2list::size()

这个函数的功能主要是返回当前list中所剩结点的个数,不包含哨兵结点。
这里有两种方案,一是list成员变量中新加入_size,但是请注意每次插入新节点时候要_size++

//size()
size_t size()//返回当前的结点数量
{
	return _size;
}

第二种方案就是遍历链表进行计数即可,这里不再多说。

5.3list::erase()

要实现erase功能,我们可以这样实现代码:

//erase()
iterator erase(iterator pos)
{
	node* cur = pos._iterator;
	node* prev = cur->_prev;
	node* next = cur->_next;

	prev->_next = next;
	next->_prev = prev;
	delete cur;
	_size--;//记得要更新_size的数值

	return iterator(next);
}

5.4pop_back,pop_front,push_back,push_front

pop_back,pop_front,push_back,push_front这些函数都可以复用insert或者erase,这里我们进行复用一下:

//push_back
//void push_back(const T& x)
//{
//	//开空间
//	node* newnode = new node(x);
//	//编辑空间
//	node* tail = _head->_prev;

//	tail->_next = newnode;
//	newnode->_prev = tail;
//	newnode->_next = _head;
//	_head->_prev = newnode;
//	_size++;
//}
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());
}

这里之前写的push_back()注释一下,我们一致复用insert就行了。

6.完善迭代器

实际上,我们之前写的迭代器只是个半成品,比如const迭代器不支持。还比如不够灵活,operator->就没有支持。
下面重点详细介绍,这才是本节博客的精华所在,请诸君静听。

6.1operator->的理解

对于自定义类型

struct A
{
	int _a;
	int _b;

	//构造函数
	A(const int& a = 0, const int& b = 0)
	{
		_a = a;
		_b = b;
	}
};

我想弄个list<A>请问此时应该怎么进行数据遍历呢?
可能你会写出下面的代码:

szg::list<A> la;
A a(1, 1);
la.push_back(a); // 有名对象尾插
la.push_back(A(2,2)); // 匿名对象尾插
la.push_back({3,3}); // CPP11新语法,多参数隐式类型转换

//访问
szg::ListIterator<A> itA = la.begin();
while (itA != la.end())
{
	std::cout << (*itA)._a << " ";
	std::cout << (*itA)._b << " ";
	itA++;
}
std::cout << std::endl;

总感觉很奇怪,但是这是正确的。

倘若这不是struct A,而是class A呢???有人可能会说提供Get_A和Get_B函数,一般CPP中不习惯写Get函数,这种JAVA是常用这样的方法的。

CPP会重载一个operator->去解决这个问题。
在ListIterator类中,我们可以写下下面代码:

T* operator->()//返回对应val值的地址
{
	return &_iterator->_val;
}

之后,我们可以这样写Test:

struct A
{
	int _a;
	int _b;

	//构造函数
	A(const int a = 0, const int b = 0)
	{
		_a = a;
		_b = b;
	}
};

szg::list<A> la;
A a(1, 1);
la.push_back(a); // 有名对象尾插
la.push_back(A(2,2)); // 匿名对象尾插
la.push_back({3,3}); // CPP11新语法,多参数隐式类型转换

//访问
szg::list<A>::iterator it = la.begin();
while (it != la.end())
{
	/*std::cout << (*itA)._a << " ";
	std::cout << (*itA)._b << " ";*/
	//std::cout << (*it)._a << " ";
	std::cout << it->_a << " ";
	std::cout << it->_b << " ";
		it++;
}
std::cout << std::endl;

这里需要注意的是,在我们调用operator的时候,编译器对其做了优化,按照逻辑我们需要写为it.operator->()->_a,这里我们可以少写一个->,更加符合我们的使用习惯。

6.2const迭代器

不知道大家发现了没有,一直以来都没用过const迭代器去遍历,实际上,在上面所写的代码中就完全没有const的影子,如果用const迭代器就会报语法错误,因为压根没有实现。

倘若你认为这样写:
在这里插入图片描述
那我可以告诉你这样的const迭代器跟非const是一样的行为,一样可以修改迭代器指向的内容。

辨析:
const迭代器和非const迭代器的区别???
const迭代器不可修改迭代器指向的内容,非const迭代器可以修改迭代器指向元素的内容。

倘若你灵机一动,说改成这样:
在这里插入图片描述
那么你这样就是让迭代器本身不可更改,而不是迭代器指向的内容不可更改。

实际上,想要正确写出const迭代器有两种方法:一是再写一个const_iterator迭代器类,再一个就是把const作为一个参数传入ListIterator中,让其根据模板自动生成一份const迭代器类。

下面来依次介绍两种方法:
对于两种方法,都是重新写一个类而已,只不过前者是自己写,后置式让编译器根据模板进行推导罢了。

template<class T>
struct ConstListIterator
{
	typedef ListNode<T> node;
	const node* _iterator;
	//itrator构造
	ConstListIterator(const node* node)
		:_iterator(node)
	{}

	//解引用重载
	const T& operator* ()const
	{
		return _iterator->_val;
	}

	const T* operator-> ()const//返回对应val值的地址
	{
		return &_iterator->_val;
	}

	//前置++重载
	ConstListIterator<T>& operator++()
	{
		_iterator = _iterator->_next;

		return *this;
	}
	//后置++重载
	//ConstListIterator<T> operator++(int)
	//{
	//	ListIterator<T> temp(*this); // 构建一个临时对象
	//	_iterator = _iterator->_next; // 让当前this指向下一个结点

	//	return temp; // 但是返回临时变量
	//} //error:temp类型错误。
	ConstListIterator<T> operator++(int)
	{
		ConstListIterator<T> tmp(*this);
		_iterator = _iterator->_next;

		return tmp;
	}

	//!=重载
	bool operator!=(const ConstListIterator<T>& l)
	{
		//return this->_iterator != l._iterator;
		return _iterator != l._iterator;
	}
	//==重载
	bool operator==(const ListIterator<T>& l)
	{
		return this->_iterator == l._iterator;
	}
};

上面就是一个写好的const迭代器类,请注意:在list中使用的时候也要用typedef更改一下名字,这样struct ConstListIterator就成为了List类中的内部类,自由使用了。

简单测试:

//test
szg::list<int> li;
li.push_back(1);
li.push_back(2);
li.push_back(3);
li.push_back(4);
li.push_back(5);

const szg::list<int> li2 = li;

szg::list<int>::const_iterator it = li2.begin();
while (it != li2.end())
{
	std::cout << *it << " ";
	it++;
}
std::cout << std::endl;

在这里插入图片描述
当然,我们也可以用第二种方式让编译器为我们写const迭代器:

template<class T, class Ref, class Pon>
struct ListIterator
{
public:
	typedef ListNode<T> node;
	node* _iterator;
public:
	//itrator构造
	ListIterator(node* node)
		:_iterator(node)
	{}

	//解引用重载
	Ref operator* ()
	{
		return _iterator->_val;
	}

	Pon operator-> ()//返回对应val值的地址
	{
		return &_iterator->_val;
	}

	//前置++重载
	ListIterator<T, Ref, Pon>& operator++()
	{
		_iterator = _iterator->_next;

		return *this;
	}
	//后置++重载
	ListIterator<T, Ref, Pon> operator++(int)
	{
		ListIterator<T, Ref, Pon> temp(*this); // 构建一个临时对象
		_iterator = _iterator->_next; // 让当前this指向下一个结点

		return temp; // 但是返回临时变量
	}

	//!=重载
	bool operator!=(const ListIterator<T, Ref, Pon>& l)
	{
		return this->_iterator != l._iterator;
	}
	//==重载
	bool operator==(const ListIterator<T, Ref, Pon>& l)
	{
		return this->_iterator == l._iterator;
	}
};

在这里插入图片描述

7.析构、拷贝构造、赋值运算符重载

7.1析构函数

~list()
{
	clear();//释放链表内容
	delete _head;//释放哨兵结点
	_head = nullptr;//置空
	_size = 0;//归零
}
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

7.2拷贝构造

注意,这个需要深拷贝,不然析构时候会析构两次出现问题。

list(const list<T>& l)
{
	empty_initialization();//申请一个哨兵位
	for (auto& node : l)//然后一直尾插数据
	{
		push_back(node);
	}
}

7.3赋值运算符重载

//赋值运算符重载
list<T>& operator=(const list<T>& l)
{
	clear();//清空内容,此时还有头节点

	const_iterator it = l.begin();//依次尾插
	while (it != l.end())
	{
		push_back(*it);
		it++;
	}

	return *this;
}

当然,也可以这样写去复用拷贝构造也行。

void swap(list<T>& l)
{
	std::swap(_head, l._head);
	std::swap(_size, l._size);
}
//赋值运算符重载
list<T>& operator=(list<T> l)
{
	swap(l);

	return *this;
}

好的,到这里我们基本就把list简化模拟实现完成了。我感觉总体来说还是稍微有点麻烦的,毕竟自己写的时候老是写着写着进到坑里去了…


好的,如果本篇文章对你有帮助,不妨点个赞~谢谢。
在这里插入图片描述


EOF

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

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

相关文章

大数据信用报告查询哪家平台的比较好?

相信在搜索大数据信用的你&#xff0c;已经因为大数据信用不好受到了挫折&#xff0c;想详细了解一下自己的大数据信用&#xff0c;但是找遍了网络上的平台之后才发现&#xff0c;很多平台都只提供查询服务&#xff0c;想要找一个专业的平台查询和讲解很困难。下面本文就为大家…

【MongoDB】2.MongoDB导入文件

目录 一、MongoDB Compass 二、mongoimport 1、安装 2、语法&#xff1a; 3、可能出现的错误 三、MongoDB的GridFS 1、介绍 2、语法 一、MongoDB Compass 这个简单&#xff0c;不做赘述 二、mongoimport Mongoimport是一个用于导入数据到MongoDB的工具&#xff0c;默…

IWDG 溢出时间计算

iwdg看门狗溢出时间&#xff0c;就是之前算过的&#xff0c;但是再记录一次 计算过程如下&#xff1a;因为iwdg是独立看门狗&#xff0c;是用的LSI, 所以在f1系列lsi的时钟频率是40khz&#xff0c;也就是Fiwdg的频率是40khz&#xff0c;频率除以psc&#xff08;分频系数&#…

计算机毕业设计 校园志愿者管理系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

C++ | Leetcode C++题解之第331题验证二叉树的前序序列化

题目&#xff1a; 题解&#xff1a; class Solution { public:bool isValidSerialization(string preorder) {int n preorder.length();int i 0;int slots 1;while (i < n) {if (slots 0) {return false;}if (preorder[i] ,) {i;} else if (preorder[i] #){slots--;i…

全面介绍 Apache Doris 数据灾备恢复机制及使用示例

引言 Apache Doris 作为一款 OLAP 实时数据仓库&#xff0c;在越来越多的中大型企业中逐步占据着主数仓这样的重要位置&#xff0c;主数仓不同于 OLAP 查询引擎的场景定位&#xff0c;对于数据的灾备恢复机制有比较高的要求&#xff0c;本篇就让我们全面的介绍和示范如何利用这…

红黑树的插入与删除

文章目录 红黑树概念红黑树的性质&#xff1a; 红黑树的插入操作情况一情况二情况三 小总结红黑树的验证红黑树的删除一.删除单孩子节点1. 删除节点颜色为黑色2. 删除颜色为红色 二. 删除叶子节点1. 删除节点为红色2.删除节点为黑色2.1兄弟节点为黑色&#xff0c;有孩子节点&am…

window下redis的安装

下载地址&#xff1a;https://github.com/tporadowski/redis/releases Windows下的.msi安装和.zip格式区别&#xff1a; .msi是Windows installer开发出来的程序安装文件&#xff0c;它可以让你安装&#xff0c;修改&#xff0c;卸载你所安装的程序。说白了.msi就是Windows in…

KeyFreeze 1.0.1.0 临时禁用电脑鼠标和键盘

KeyFreeze 是一个免费实用的临时禁用电脑鼠标和键盘的软件&#xff1b;软件支持 Windows 。 它完全免费&#xff0c;体积非常小巧&#xff0c;使用场景是当你在某些时候需要临时禁用鼠标和键盘&#xff0c;让鼠标和键盘无法使用&#xff0c;防止误触。 开软件只有一个按钮&am…

stm32入门-----硬件SPI读写W25Q64

目录 前言 一、相关库函数介绍 1.初始化 2.写入数据 3.接收数据 4.获取标志位 二、软件SPI读写W25Q64 前言 上一期我们学习了stm32的SPI外设&#xff08;上一期链接&#xff1a;stm32入门-----硬件SPI外设-CSDN博客&#xff09;&#xff0c;那么我们本期就来…

最近很火的FLUX.1文生图模型本地一键整合包SwarmUI,schnell量化版,6G显存可畅玩的FLUX.1

最近一个新的文生图模型突然火出圈&#xff0c;它就是由Black Forest Labs&#xff08;黑森林实验室&#xff09;团队开发的FLUX.1。 Black Forest Labs估计很多人都没听说过&#xff0c;还以为是新生的创业团队&#xff0c;现在就先来说一说Black Forest Labs。玩过AI绘画的&a…

【Ansible 学习之旅】Ansible 介绍和架构

目录 Ansible 发展Ansible 介绍Ansible 组成 Ansible 发展 2012-03-09&#xff0c;发布0.0.1版 2015-10-17&#xff0c;Red Hat宣布1.5亿美元收购 官网&#xff1a;https://www.ansible.com/ 官方文档&#xff1a;https://docs.ansible.com/ Ansible 介绍 Ansible 可自动管理…

云存储技术:HBase HDFS 无感知迁移方案

在大数据生态系统中&#xff0c;HBase 和 HDFS 是两个关键组件。HBase 是一个分布式列式数据库&#xff0c;常用于实时读写大规模数据&#xff1b;HDFS 是一个高可靠的分布式文件系统&#xff0c;用于存储海量数据。 1、背景 随着业务的发展和技术的进步&#xff0c;可能需要对…

HDMI vs DP:LED显示屏背后的高清传输大揭秘

在如今数字化高速发展的时代&#xff0c;LED显示屏以其高亮度、高清晰度、长寿命等优点&#xff0c;广泛应用于广告、会议、娱乐等多个领域。然而&#xff0c;要让这些绚丽多彩的画面完美呈现&#xff0c;离不开背后默默工作的接口技术。今天&#xff0c;我们就来揭开LED显示屏…

前端react集成OIDC

文章目录 OpenID Connect (OIDC)3种 授权模式 【服务端】express 集成OIDC【前端】react 集成OIDCoidc-client-js库 原生集成react-oidc-context 库非组件获取user信息 OAuth 2.0 协议主要用于资源授权。 OpenID Connect (OIDC) https://openid.net/specs/openid-connect-core…

【安当产品应用案例100集】007-工业控制系统防勒索解决方案-安当RDM防勒索

《工业控制系统网络安全防护指南》是由工业和信息化部于2024年1月19日发布&#xff0c;旨在指导企业提升工业控制系统网络安全防护水平&#xff0c;确保新型工业化发展的安全根基。该指南适用于使用和运营工业控制系统的企业&#xff0c;包括但不限于可编程逻辑控制器&#xff…

【生成式人工智能-十-文字、图片、声音生成策略】

人工智能生成文字、图片、声音的方法 生成的本质生成的策略文字AR (Autoregressive Generation)图像和视频 NAR(Non-Autoregressive Generation)解决NAR生成品质低的问题 AR NAR 生成的本质 文字、图像、声音的生成的本质&#xff0c;就是给模型一个输入&#xff0c;模型把基…

大模型应用中的幻觉问题是什么?

大模型应用中的幻觉问题是什么&#xff1f; 在现代自然语言处理领域&#xff0c;大语言模型&#xff08;如GPT系列&#xff09;以其惊人的生成能力和语言理解能力被广泛应用。然而&#xff0c;随着这些模型的广泛使用&#xff0c;幻觉问题逐渐显现出其对实际应用的潜在影响。本…

怎样快速查询数家公司是否存在关联关系?

▶关联关系的查询是企业稳健运营和长期发展的关键环节 企业在关键时期需要查询数家公司是否存在关联关系&#xff0c;以确保合规性和透明度。这通常发生在年度审计、税务申报、并购活动、上市准备、风险评估和法律诉讼时。监管合规性检查、内部控制加强、市场策略制定、资金管…

加速区域市场扩张,ATFX任命Amer Zino为中东和北非业务发展总监

全球领先的差价合约经纪商ATFX再度向世界展示了其吸纳行业顶尖复合型人才的决心和能力。日前&#xff0c;ATFX旗下机构业务ATFX Connect宣布一项重磅人事任命&#xff0c;行业杰出领袖Amer Zino加入公司&#xff0c;出任中东和北非业务发展总监一职&#xff0c;并将常驻工作地阿…