当前位置: 首页 > news >正文

【STL学习】(6)list的模拟

前言

list的底层是带头双向循环链表,在数据结构专栏我们使用C语言简单模拟实现过,这里使用C++模拟实现也是大同小异的。

建议:阅读本文如有困难的,可以点击下面链接,复习带头双向循环链表:

C语言实现带头双向循环链表

一、list的基本框架

1. 将list封装

使用命名空间将我们模拟实现的list封装,避免命名冲突!

2. 链表结点

  1. 我们模拟的list也要和库中的list一样,需要是“通用的”,所以需要将其定义为类模板。
  2. 因为一会需要使用结点的成员,所以结点使用struct定义。
  3. new一个新节点的时候,它会自动调用构造函数去初始化新节点。
//将其封装在wjs命名空间中,与STL中的list区别开
namespace wjs
{
	//链表节点
	//因为后期需要频繁访问节点的成员,所以使用struct定义
	template<class T>
	struct list_node
	{
		//注意:类模板中类名不是类型,需要显式实例化
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;
		
		//结点的默认构造函数——初始化新结点
		list_node(const T& val = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_val(val)
		{}
	};
}

3. list类模板的基本框架

  1. 因为写类模板类型的时候大家容易忘记实例化,所以类模板喜欢typedef
  2. 先实现一个list的默认构造函数和尾插让list简单运行起来
  3. 在类模板中类名也是类型,但在模板之外类名不是类型,所以建议所有类模板还是显式实例化使用。
//将其封装在wjs命名空间中,与STL中的list区别开
namespace wjs
{
	//带头双向循环链表
	template<class T>
	class list
	{
		//因为类模板的类名不是类型,需要实例化,但是我们写的时候容易忘记,所以模板喜欢typedef
		typedef list_node<T> Node;
	public:
		//list的默认构造函数——初始化链表
		list()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}

		//list的尾插
		void push_back(const T& x)
		{
			//逻辑草图:tail《——》newnode《——》_head
			//①找尾
			Node* tail = _head->_prev;
			//②创建新节点
			Node* newnode = new Node(x);
			//③链接
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}
	private:
		//指向哨兵位结点的指针
		Node* _head;
	};
}

4. list的迭代器(重点)

(1)普通迭代器

  1. 前面我们学习的string和vector的底层是数组,空间是连续,所以迭代器可以是原生指针。
  2. list的迭代器也和vector一样吗,是原生指针Node*吗,答案当然是不可以的,因为:
    • Node*解引用得到的是结点,而迭代器解引用得到的是该位置的数据
    • Node*++不能移动到下一位置,而迭代器++移动到下一位置
  3. 所以list的迭代器是一种自定义类型,通过自定义类型封装,然后通过运算符重载改变它的行为(即让它满足我们迭代器的需求)
  4. list的迭代器本质上是结点指针的自定义类型,通过封装,运算符重载使之解引用得到数据,++移动到下一个位置等
  5. 因为结点是list创建和释放的,不属于迭代器,迭代器不用去管它的创建和释放,所以迭代器的拷贝构造、析构函数使用默认生成的即可,不需要自己实现
//将其封装在wjs命名空间中,与STL中的list区别开
namespace wjs
{
	//list的迭代器——其实是一个结点的自定义类型
	//我们可以通过运算符重载(改变它原先的行为),使之满足我们的需求
	template<class T>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		Node* _node;

		//构造函数——通过一个结点的指针即可初始化一个迭代器
		__list_iterator(Node* node)
			:_node(node)
		{}

		//重载运算符解引用——iterator解引用得到结点的数据
		T& operator*()
		{
			return _node->_val;
		}

		//重载运算符++——iterator++得到下一结点的位置
		//①前置++
		__list_iterator<T>& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		//②后置++——为了区分后置+我们使用int占位
		__list_iterator<T>& operator++(int)
		{
			__list_iterator<T> tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		//重载运算符!=——迭代器不相等返回真
		//注意:end是传值返回,所以迭代器具有常性,所以形参需要const修饰
		bool operator!=(const __list_iterator<T>& it)
		{
			return _node != it._node;
		}

		//重载运算符==——迭代器相等返回真
		bool operator==(const __list_iterator<T>& it)
		{
			return _node == it._node;
		}
	};

	//带头双向循环链表
	template<class T>
	class list
	{
		//因为类模板的类名不是类型,需要实例化,但是我们写的时候容易忘记,所以模板喜欢typedef
		typedef list_node<T> Node;
	public:
		//重命名list的迭代器——虽然底层实现是不一样的,但是迭代器的用法是一样的!
		typedef __list_iterator<T> iterator;
		iterator begin()
		{
			//单参数的构造函数支持隐式类型的转换
			//所以直接传结点的指针就可以转换为迭代器
			return _head->_next;
			//return iterator(_head->_next);
		}
		iterator end()
		{
			//单参数的构造函数支持隐式类型的转换
			//所以直接传结点的指针就可以转换为迭代器
			return _head;
			//return iterator(_head);
		}
	private:
		//指向哨兵位结点的指针
		Node* _head;
	};
}

(2)const迭代器

  1. 普通迭代器和const迭代器的区别:
    • 普通对象调用普通迭代器,迭代器可读可写
    • const对象调用const迭代器,迭代器只可读不可写
  2. 如下图代码,我们可以这样设计const迭代器吗?在这里插入图片描述
    答案自然是不可以的,这样设计的迭代器本身不可以修改!
  3. const迭代器是指向的内容不可修改,但是迭代器本身是可以修改的。
  4. 即const迭代器和普通迭代器唯一的区别是它解引用返回的是const对象,其它的都与普通迭代器一样。在这里插入图片描述
  5. 那我们粘贴一份普通迭代器代码,将解引用模块修改为const迭代器的,再将其重命名为const_iterator是不是就可以了?

    答案:虽然这样可以满足我们const迭代器的需求,但是这样设计的太冗余了,STL库中并没有这样设计。
  6. STL库中是通过模板来解决的,普通迭代器和const迭代器只有operator*的返回类型不一样,那我们可以通过增加一个模板参数来控制即可。
  7. 模板增加了一个模板参数,那在使用了类模板类型都需要更改,我们可以将其typedef一改全改,这样后续类模板的参数发生变换我们也只需要更改typedef即可。
namespace wjs
{
	//list迭代器
	template<class T, class Ref>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref> self;
		
		Node* _node;
		
		__list_iterator(Node* node)
			:_node(node)
		{}

		//重载运算符解引用——iterator解引用得到结点的数据
		返回普通迭代器——可读可写
		//T& operator*()
		//{
		//	return _node->_val;
		//}
		返回const迭代器——只可读不可写
		//const T& operator*()
		//{
		//	return _node->_val;
		//}
		//普通迭代器和const迭代器只有返回类型不一样,我们将其设计成模板参数,通过传参来控制其类型即可!
		Ref operator*()
		{
			return _node->_val;
		}

		//重载运算符++——iterator++得到下一结点的位置
		//①前置++
		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		//②后置++——为了区分后置+我们使用int占位
		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		//重载运算符!=——迭代器不相等返回真
		//注意:end是传值返回,所以迭代器具有常性,所以形参需要const修饰
		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
		typedef list_node<T> Node;
	public:
		//重命名list的迭代器——虽然底层实现是不一样的,但是迭代器的用法是一样的!
		//普通迭代器
		typedef __list_iterator<T, T&> iterator;
		//const迭代器
		typedef __list_iterator<T, const T&> const_iterator;
		iterator begin()
		{
			//单参数的构造函数支持隐式类型的转换
			//所以直接传结点的指针就可以转换为迭代器
			return _head->_next;
			//return iterator(_head->_next);
		}
		iterator end()
		{
			//单参数的构造函数支持隐式类型的转换
			//所以直接传结点的指针就可以转换为迭代器
			return _head;
			//return iterator(_head);
		}
		const_iterator begin()const
		{
			return _head->_next;
		}
		const_iterator end()const
		{
			return _head;
		}
	};
}
  1. 迭代器其本质是模拟的指针的行为,所有的容器都期望提供一种像指针去访问容器的方式。
  2. 容器将迭代器进行了封装,在上层我们都使用begin、end等来获取迭代器。在这里插入图片描述
  3. 迭代器类似指针,指针可以通过箭头运算符访问自定类类型的成员变量,那迭代器自然也可以,list的迭代器是结点的自定类类型,所以list的箭头运算符需要我们自己重载运算符!
  4. 对于point->mem表达式,point必须是指向类对象的指针或者是一个重载了operator->的类对象。
  5. 根据point类型的不一样,point分别等价于:
    • point是指针,则我们应用内置的箭头运算符,表达式等价于(*point).mem。首先解引用该指针,然后从所得对象中获取指定的成员。
    • point是定义了operator->的类的一个对象,则使用point.operator->()的结果来获取mem。其中,如果该结果是指针,则执行则应用内置的箭头运算符(因为运算符重载要求可读性,所以编译器特殊处理,省略了一个->);如果该结果本身含有重载的operator->(),则重复调用当前步骤。
  6. 重载的箭头运算符的返回类型:
    • 返回类的指针
    • 自定义箭头运算符的类的对象
  7. 所有调用operator->()语法上应该连续写两个箭头运算符,第一个调用operator->()得到一个类的指针,第二个调用内置的箭头运算符访问类的成员,但是为了可读性,所以编译器做了特殊处理,省略一个->。
  8. list迭代器重载的箭头运算符,重载后指向结点的内容的地址。在这里插入图片描述
  9. list迭代器重载的箭头运算符,普通迭代器返回T*,const迭代器返回const T*,所以和operator*一样,这里我们也将其设计成模板参数
  10. 学到这里我们就可以明白为什么库中迭代器的类模板设计了3个模板参数:
    • 第一个模板参数T:迭代器的类型是不一样的
    • 第二个模板参数Ref:迭代器解引用的返回类型不一样
    • 第三个模板参数Ptr:迭代器重载的箭头运算符的返回类型不一样
namespace wjs
{
	//list的迭代器——其实是一个结点的自定义类型
	//我们可以通过运算符重载(改变它原先的行为),使之满足我们的需求
	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		//模板类在使用时需要显式实例化,现在我们给类模板增加了一个模板参数,那所有使用类模板的地方都需要更改
		//所以我们使用typedef重命名,这样后续类模板的参数发生改变我们也只需要更改typedef即可
		typedef __list_iterator<T, Ref, Ptr> self;
		Node* _node;

		//构造函数——通过一个结点的指针即可初始化一个迭代器
		__list_iterator(Node* node)
			:_node(node)
		{}
		
		//普通迭代器和const迭代器只有返回类型不一样,我们将其设计成模板参数,通过传参来控制其类型即可!
		Ref operator*()
		{
			return _node->_val;
		}

		//重载运算符->——返回指向结点内容的指针(即类的地址)
		//普通迭代器返回T*,const迭代器返回const T*,所以和operator*一样,这里我们也将其设计成模板参数
		Ptr operator->()
		{
			return &_node->_val;
		}

		//重载运算符++——iterator++得到下一结点的位置
		//①前置++
		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		//②后置++——为了区分后置+我们使用int占位
		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		//重载运算符!=——迭代器不相等返回真
		//注意:end是传值返回,所以迭代器具有常性,所以形参需要const修饰
		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
		typedef list_node<T> Node;
	public:
		//重命名list的迭代器——虽然底层实现是不一样的,但是迭代器的用法是一样的!
		//普通迭代器
		typedef __list_iterator<T, T&, T*> iterator;
		//const迭代器
		typedef __list_iterator<T, const T&, const T*> const_iterator;
		iterator begin()
		{
			//单参数的构造函数支持隐式类型的转换
			//所以直接传结点的指针就可以转换为迭代器
			return _head->_next;
			//return iterator(_head->_next);
		}
		iterator end()
		{
			//单参数的构造函数支持隐式类型的转换
			//所以直接传结点的指针就可以转换为迭代器
			return _head;
			//return iterator(_head);
		}
		const_iterator begin()const
		{
			return _head->_next;
		}
		const_iterator end()const
		{
			return _head;
		}
	};
}

5. list的插入删除

(1)insert&push_back&push_front

  1. insert:在pos迭代器之前插入一个新节点
  2. list的空间不连续是按需申请,insert之后不会导致迭代器失效问题
  3. 我们只需要实现insert后,头插和尾插复用insert即可
//insert:在pos位置之前插入
iterator insert(iterator pos, const T& x)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(x);
	//链接:prev《——》newnode《——》cur
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
	//插入之后,返回新插入节点的迭代器
	return newnode;
}

//list的尾插:复用insert
void push_back(const T& x)
{
	insert(end(), x);
}
//list的头插:复用insert
void push_front(const T& x)
{
	insert(begin(), x);
}

(2)erase&pop_back&pop_front

  1. erase:删除pos迭代器指向的结点,注意哨兵位不能删
  2. erase之后迭代器失效,我们通过返回下一个位置的迭代器解决失效问题
  3. 尾删和头删复用erase即可
//erase:删除pos位置结点
iterator erase(iterator pos)
{
	//哨兵位不能删
	assert(pos != end());
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;
	//链接:prev《——》next
	prev->_next = next;
	next->_prev = prev;
	//删除cur
	delete cur;
	//删除之后迭代器失效,返回下一个位置的迭代器
	return next;
}

//list的尾删:复用erase
void pop_back()
{
	erase(--end());
}
//list的头删:复用erase
void pop_front()
{
	erase(begin());
}

(3)clear

  1. clear:将所有有效结点释放,但保留哨兵位
//list的清理:将所有有效结点释放,但保留哨兵位(因为链表的空间不是连续的,按需申请,所以不要了可以将其释放)
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		//erase之后迭代器失效,我们通过接收它的返回值解决它的失效问题
		it = erase(it);
	}
}

(4)size

  1. size:返回list有效节点的个数
  2. 方式1:通过遍历链表得到,但是时间复杂度为O(N),效率低
  3. 方式2:list增加一个成员变量_size用于存储有效节点的个数
//list的大小:有效节点的个数
size_t size()
{
	//方式1:通过遍历链表得到有效结点个数
	size_t sz = 0;
	iterator it = begin();
	while (it != end())
	{
		++it;
		++sz;
	}
	return sz;
	//方式2:可以增加一个成员变量_size用于存储有效结点个数,返回_size即可。
}

6. list的拷贝构造&析构

(1)析构函数

  1. list设计动态资源申请,所以需要显示实现析构函数
  2. delete释放完空间之后与free一样,不会改变指向动态空间的指针变量,有危险,建议释放完之后将其置为空
//析构函数:释放整个list,包括哨兵位
~list()
{
	//复用clear:释放所有的有效节点
	clear();
	//释放哨兵位
	delete _head;
	//释放之后_head为野指针,将其置为空
	_head = nullptr;
}

(2)拷贝构造函数

  1. list涉及动态资源申请,需要深拷贝,所以需要我们显示实现拷贝构造
  2. 拷贝构造的深拷贝有如下两种实现方式:
    • 传统写法:自己开空间,自己拷贝
    • 现代写法:把工作交给别人,别人完成了再获取(即直接拿别人的结果)
  3. 我们发现构造函数和拷贝构造有一些代码重复(成员变量初始化),我们可以将其提炼出来将其封装为一个单独模块,库里面也是这样做的。在这里插入图片描述
  4. operator=也是一样的,当涉及深拷贝时,需要我们显示实现。
  5. 现代写法和传统写法并没有效率上的差别,只不过是复用代码,让代码简洁了。
//成员变量的初始化
void empty_init()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
}

//拷贝构造函数
//传统写法:自己开空间,自己拷贝
list(const list<T>& lt)
{
	//自己开空间
	empty_init();
	//自己拷贝
	for (auto& e : lt)
	{
		push_back(e);
	}
}

//重载赋值运算符
list<T>& operator=(list<T> lt)
{
	//我们已经直接叫形参帮我们开好空间,并且做了拷贝
	//所以这里我们直接交换形参,拿到结果即可
	swap(lt);
	return *this;
}

//交换两个链表
void swap(list<T> lt)
{
	std::swap(_head, lt._head);
}

list的接口我们模拟这些常用的即可,模拟只是让我们更加深入的去了解list,并不是去造一个更好的轮子!

完整代码参考:

#pragma once

#include<assert.h>

//将其封装在wjs命名空间中,与STL中的list区别开
namespace wjs
{
	//链表节点
	//因为后期需要频繁访问节点的成员,所以使用struct定义
	template<class T>
	struct list_node
	{
		//注意:类模板中类名不是类型,需要显式实例化
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;

		//结点的默认构造函数——初始化新结点
		list_node(const T& val = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_val(val)
		{}
	};

	//list的迭代器——其实是一个结点的自定义类型
	//我们可以通过运算符重载(改变它原先的行为),使之满足我们的需求
	//普通迭代器
	//typedef __list_iterator<T, T&> iterator;
	//const迭代器
	//typedef __list_iterator<T, const T&> const_iterator;
	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		//模板类在使用时需要显式实例化,现在我们给类模板增加了一个模板参数,那所有使用类模板的地方都需要更改
		//所以我们使用typedef重命名,这样后续类模板的参数发生改变我们也只需要更改typedef即可
		typedef __list_iterator<T, Ref, Ptr> self;
		Node* _node;

		//构造函数——通过一个结点的指针即可初始化一个迭代器
		__list_iterator(Node* node)
			:_node(node)
		{}

		//重载运算符解引用——iterator解引用得到结点的数据
		返回普通迭代器——可读可写
		//T& operator*()
		//{
		//	return _node->_val;
		//}
		返回const迭代器——只可读不可写
		//const T& operator*()
		//{
		//	return _node->_val;
		//}
		//普通迭代器和const迭代器只有返回类型不一样,我们将其设计成模板参数,通过传参来控制其类型即可!
		Ref operator*()
		{
			return _node->_val;
		}

		//重载运算符->——返回指向结点内容的指针(即类的地址)
		//普通迭代器返回T*,const迭代器返回const T*,所以和operator*一样,这里我们也将其设计成模板参数
		Ptr operator->()
		{
			return &_node->_val;
		}

		//重载运算符++——iterator++得到下一结点的位置
		//①前置++
		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		//②后置++——为了区分后置+我们使用int占位
		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		//重载运算符--——iterator--得到前一结点的位置
		//①前置--
		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		//②后置--——为了区分后置-我们使用int占位
		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		//重载运算符!=——迭代器不相等返回真
		//注意:end是传值返回,所以迭代器具有常性,所以形参需要const修饰
		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
		typedef list_node<T> Node;
	public:
		//重命名list的迭代器——虽然底层实现是不一样的,但是迭代器的用法是一样的!
		//普通迭代器
		typedef __list_iterator<T, T&, T*> iterator;
		//const迭代器
		typedef __list_iterator<T, const T&, const T*> const_iterator;
		iterator begin()
		{
			//单参数的构造函数支持隐式类型的转换
			//所以直接传结点的指针就可以转换为迭代器
			return _head->_next;
			//return iterator(_head->_next);
		}
		iterator end()
		{
			//单参数的构造函数支持隐式类型的转换
			//所以直接传结点的指针就可以转换为迭代器
			return _head;
			//return iterator(_head);
		}
		const_iterator begin()const
		{
			return _head->_next;
		}
		const_iterator end()const
		{
			return _head;
		}

		//如下:我们可以这样设计迭代器吗?
		//不可以,这样设计是迭代器本身不可修改,
		//而const迭代器是期望指向的内容不可修改,本身是可以修改的!
		//typedef const __list_iterator<T> const_iterator;

		//list的默认构造函数——初始化链表
		list()
		{
			empty_init();
		}

		//insert:在pos位置之前插入
		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);
			//链接:prev《——》newnode《——》cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			//插入之后,返回新插入节点的迭代器
			return newnode;
		}

		//erase:删除pos位置结点
		iterator erase(iterator pos)
		{
			//哨兵位不能删
			assert(pos != end());
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			//链接:prev《——》next
			prev->_next = next;
			next->_prev = prev;
			//删除cur
			delete cur;
			//删除之后迭代器失效,返回下一个位置的迭代器
			return next;
		}

		//list的尾插
		//void push_back(const T& x)
		//{
		//	//逻辑草图:tail《——》newnode《——》_head
		//	//①找尾
		//	Node* tail = _head->_prev;
		//	//②创建新节点
		//	Node* newnode = new Node(x);
		//	//③链接
		//	tail->_next = newnode;
		//	newnode->_prev = tail;
		//	newnode->_next = _head;
		//	_head->_prev = newnode;
		//}

		//list的尾插:复用insert
		void push_back(const T& x)
		{
			insert(end(), x);
		}
		//list的头插:复用insert
		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		//list的尾删:复用erase
		void pop_back()
		{
			erase(--end());
		}
		//list的头删:复用erase
		void pop_front()
		{
			erase(begin());
		}

		//list的大小:有效节点的个数
		size_t size()
		{
			//方式1:通过遍历链表得到有效结点个数
			size_t sz = 0;
			iterator it = begin();
			while (it != end())
			{
				++it;
				++sz;
			}
			return sz;
			//方式2:可以增加一个成员变量_size用于存储有效结点个数,返回_size即可。
		}

		//list的清理:将所有有效结点释放,但保留哨兵位(因为链表的空间不是连续的,按需申请,所以不要了可以将其释放)
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				//erase之后迭代器失效,我们通过接收它的返回值解决它的失效问题
				it = erase(it);
			}
		}

		//析构函数:释放整个list,包括哨兵位
		~list()
		{
			//复用clear:释放所有的有效节点
			clear();
			//释放哨兵位
			delete _head;
			//释放之后_head为野指针,将其置为空
			_head = nullptr;
		}

		//成员变量的初始化
		void empty_init()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}

		//拷贝构造函数
		//传统写法:自己开空间,自己拷贝
		list(const list<T>& lt)
		{
			//自己开空间
			empty_init();
			//自己拷贝
			for (auto& e : lt)
			{
				push_back(e);
			}
		}

		//重载赋值运算符
		list<T>& operator=(list<T> lt)
		{
			//我们已经直接叫形参帮我们开好空间,并且做了拷贝
			//所以这里我们直接交换形参,拿到结果即可
			swap(lt);
			return *this;
		}

		//交换两个链表
		void swap(list<T> lt)
		{
			std::swap(_head, lt._head);
		}
	private:
		//指向哨兵位结点的指针
		Node* _head;
	};
}

相关文章:

  • 基于Spring Security 6的OAuth2 系列之二十三 - 高级特性--TLS客户端认证方法之二
  • 解密RAG系统排序优化:从基础原理到生产实践
  • 洛谷每日1题-------Day3__级数求和
  • KNN算法优化实战分享:从原理到工程化落地的深度解析
  • PowerShell 执行策略:fnm管理软件安装nodejs无法运行npm,错误信息:about_Execution_Policies
  • 279.完全平方数
  • 【python】01_写在前面的话
  • 【12】智能合约开发入门
  • 车载DoIP诊断框架 --- 连接 DoIP ECU/车辆的故障排除
  • 【Python】3. python包的更新维护 编写项目介绍,更新日志,解决项目介绍乱码的问题(保姆级图文)
  • Windows下安装ollama+deepseek+maxkb
  • 用Python3脚本实现Excel数据到TXT文件的智能转换:自动化办公新姿势
  • 深入miniqmt:创建交易对象的完整指南
  • Linux内核自定义协议族开发指南:理解net_device_ops、proto_ops与net_proto_family
  • 橄榄球、棒球项目排名·棒球1号位
  • MySQL数据库入门到大蛇尚硅谷宋红康老师笔记 高级篇 part 4
  • 2024年第十五届蓝桥杯大赛软件赛省赛Python大学A组真题解析
  • NLP09-加强1-对比SVM
  • P10108 [GESP202312 六级] 闯关游戏
  • 爬虫抓取数据时如何处理异常?
  • 商务部再回应中美经贸高层会谈:美方要拿出诚意、拿出行动
  • 西安碑林博物馆票价将调至85元,工作人员:10元属于改扩建期间惠民票
  • 外交部:印巴都表示不希望局势升级,望双方都能保持冷静克制
  • 吴清:加强监管的同时传递监管温度,尽力帮助受影响企业应对美加征关税的冲击
  • 李云泽:小微企业融资协调工作机制已发放贷款12.6万亿元
  • 金融监管总局将推出8项增量政策:涉房地产金融、险资入市、稳外贸等