【C++】 list的模拟实现

前言

STL list 容器,又称双向链表容器,即该容器的底层是以双向链表的形式实现的。这意味着,list 容器中的元素可以分散存储在内存空间里,而不是必须存储在一整块连续的内存空间中

在这里插入图片描述

一. 基本框架

正如我们在学习数据结构的带头双向链表时,链表节点的结构体定义里,应该有指向下一个节点的next指针,指向上一个节点的prev指针,和该节点的值date
这是一个节点的,作为整体链表的框架,我们需要一个哨兵卫的头结点head。其内部不存储数据,仅有链接头尾的作用。

代码如下

//节点的结构体
template<class T>
struct list_node
{
	list_node<T>*_next;
	lsit_node<T>*_prev;
	T _date;
};

//链表的框架
template<class T>
class list
{
	typedef list_node<T> node;
public:

private:
	node* head;
};

因为list同样要和vector一样,适配不同的数据类型,所以我们采用类模板,结构体处也需要使用模板,这样才可以统一参数。
另外为了见名知意,所以我们也可以对一些较长的,较复杂的参数名进行重命名。

二. 数据写入&遍历

1. 数据写入

定义好基本框架后,接下来我们需要可以实例化对象完成数据存储。
那么实例化对象需要构造函数

//list_node的默认构造
template<class T>
struct list_node
{
	list_node<T>*_next;
	list_node<T>*_prev;
	T _date;

	list_node(const T&x=T())
		:_next(nullptr)
		, _prev(nullptr)
		, _date(x)
	{}
};

//list的构造函数
list()
{
	_head = new node;
	_head->next = _head;
	_head->prev = _head;
}

因为是实例化的是哨兵卫的头结点,所以最开始,我们让_next和_prev指向自己
在这里插入图片描述
对于最开始的写入,我们先编写最简单的尾插

void push_back(const T&date)
{
	node*tail = _head->prev;
	node*new_node = new node(date);
	
    //断开旧尾和头的链接
	tail->_next = new_node;
	new_node->_prev = tail;
	//链接新尾
	new_node->_next = head;
	head->_prev = new_node;
}

在这里插入图片描述
简单测试一下

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

在这里插入图片描述
这就是最简单的尾插

2. 数据遍历

那么接下来,我们如何遍历这个list呢?
根据STL一贯作风,我们需要迭代器
所以我们需要封装一下迭代器。

(1). 迭代器

首先,迭代器本质是指针,所以其内部变量应该是list_node的指针
基本的构造函数,然后我们使用迭代器遍历需要*解引用,++迭代器,迭代器!=end
迭代器又是封装,所以这些运算符无法直接使用,所以我们需要对他们进行重载

代码如下

template<class T>
struct _list_iterator
{
	typedef list_node<T> node;
	typedef _list_iterator<T> self;//因为需要返回迭代器,所以重命名一下
	//成员变量
	node *_node;

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

	//operator*重载
	T&operator*()
	{
		return _node->_date;
	}

	//operator++重载
	self operator++()
	{
		_node = _node->_next;
		return *this;//返回迭代器本身
	}

	//operator!=重载
	bool operator!=(const self&s)
	{
		return _node != s._node;
	}

};

接下来,我们需要将迭代器包含在list中,并且是公有,这样才能直接使用。
然后就像以往迭代器遍历,我们还需要begin()和end()

//重命名
typedef _list_iterator<T> iterator;

iterator begin()
{
	return iterator(_head->_next);
}

iterator end()
{
	return iterator(_head);
}

因为迭代器访问是左闭右开,所以end应该返回哨兵卫的头结点,这样最后一个元素才可以被访问到

测试

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

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

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

遍历需要++it,不能it++,因为operator++参数列表没有参数是重载前置++

在这里插入图片描述

(2). 零碎知识点

在这里插入图片描述
在以往的STL模拟实现里,深浅拷贝一直难点。这处,基于我们目前编写的代码,明显是一个浅拷贝。但是程序却没有崩溃。这是为什么呢?
在这里插入图片描述
原因是:因为我们并没有写迭代器的析构函数,所以程序结束时,系统会调用默认析构,而默认析构只有将it=NULL置空这样的操作,并没有delete释放迭代器指向的空间。所以不存在重复释放空间的操作。
这里也能说明,迭代器的功能之一是支持我们对封装的数据做访问,遍历,只是一个工具,并不具备操作数据存储的空间的权限。

(3). 其他重载

//operator后置++重载
self operator++(int)
{
	self tmp(*this);
	_node = _node->_next;

	return tmp;
}

//operator前置--重载
self& operator --()
{
_node = _node->_prev;
return *this;//返回迭代器本身
}

//operator后置--重载
self operator--(int)
{
	self tmp(*this);
	_node = _node->_prev;

	return tmp;
}

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

三. const 迭代器

函数传参,我们必定会用到const list&lt,那么const的迭代器就是我们需要实现的。
注意:const迭代器的定义不能是以下形式

在这里插入图片描述
在这里插入图片描述
因为const修饰的是T*,导致使得指针变成常量,也就是指针常量,
那么如何实现呢?
其实我们可以重新定义一个类,内部属性和list_iterator几乎一样。然后在operator*等代码更改返回值就好

	template<class T>
	struct _list_const_iterator
	{
		typedef list_node<T> node;
		typedef _list_const_iterator<T> self;//因为需要返回迭代器,所以重命名一下 

		//成员变量
		node* _node;

		//构造函数
		_list_const_iterator(node* x)
			:_node(x)
		{}

		//operator*重载
		const T&operator*()
		{
			return _node->_date;
		}

		//operator前置++重载
		self& operator ++()
		{
			_node = _node->_next;
			return *this;//返回迭代器本身
		}

		//operator后置++重载
		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;

			return tmp;
		}

		//operator前置--重载
		self& operator --()
		{
			_node = _node->_prev;
			return *this;//返回迭代器本身
		}

		//operator后置--重载
		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;

			return tmp;
		}

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

		//operator!=重载
		bool operator!=(const self&s)
		{
			return _node != s._node;
		}



	};

注意:operator*的返回值要加const,因为要限制引用内部的值不能修改

list类内也要加相应的,返回const迭代器的begin 和end函数

在这里插入图片描述
但是这样代码有些冗余了,两个迭代器只有operator*的返回值不一样。
此处,我们使用两个模板参数,解决这个问题
代码如下

在这里插入图片描述
然后list中迭代器的定义是这样的
在这里插入图片描述
因为模板会自动匹配类型,当我们调用const_iterator时,_list_iterator中的Ref就是const T&,然后operator返回值就是const T&了。
而当我们调用iterator时,_list_iterator中的Ref是T&,operator
返回值是T&。
完美解决问题。
测试

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

1. 迭代器参数模板的第三个参数

以上,我们只测试了int内置数据类型,但list的模板参数还可以是自定义数据类型
比如,我们定义一个AA类,将其作为参数模板实例化一个list,并对其进行遍历。

struct AA
{
	int _a1;
	int _a2;

	AA(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{}
};

void list_test2()
{
	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 << " ";
		it++;
	}
	cout << endl;
}

但程序无法运行,因为我们并没有重载流插入
在这里插入图片描述
解决方法之一呢,就是在全局域,重载一个operator<<AA的函数。
解决方法二,就是先*解引用,再 . 访问
在这里插入图片描述
这里还有一个小知识点
这里我们如果运行,编译是不通过的

在这里插入图片描述

原因是因为,AA类没有写无参构造

在这里插入图片描述
因为list_node的构造需要使用匿名对象,而匿名对象需要调用相应数据类型的无参构造,但AA类没有无参构造,所以报错。
我们只需要在AA内的构造函数提供全缺省就好了
在这里插入图片描述
程序成功运行
在这里插入图片描述

但是,迭代器模拟的是指针,指针如果是自定义数据类型的指针,那么他的访问方式应该是 - >。所以,为了提高代码的可读性,我们需要重载operator->

那么如何操作呢?其实只要返回_date的指针就好了
在这里插入图片描述
在这里插入图片描述

但是这就有一点奇怪了,因为operator->不是应该先->,然后返回_date的指针,然后再->解引用?
在这里插入图片描述
这是其实是编译器为了可读性进行了优化。

但是考虑到还有const的返回值,函数又不能凭借返回值实现重载,所以类模板的参数列表还可以使用第三个参数

在这里插入图片描述
在这里插入图片描述

四. 插入&删除

list常用的函数接口就是插入和删除,这样也可以囊括头插和头删。

1. 插入

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

	node*new_node = new node(x);

	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

后续我们通过find返回指定位置的迭代器,就可以实现O(1)的插入删除

测试

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

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

		auto pos = lt.begin();
		pos++;

		lt.insert(pos, 20);

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

	}

在2位置前插入20

在这里插入图片描述

这样头插尾插就可以复用insert了

void push_back(const T&date)
{
	//复用insert
	insert(end(), date);
}
		
void push_front(const T&date)
{
	insert(begin(), date);
}

测试

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

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

		lt.push_front(10);
		lt.push_front(20);


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

	}

在这里插入图片描述
list的insert并不会造成迭代器失效,因为没有改变指针指向

2. 删除

iterator erase(iterator pos)
{
	assert(pos != end());

	node*cur = pos._node;
	node*prev = cur->_prev;
	node*next = cur->_next;

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

	return iterator(next);
}

测试

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

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

		auto pos = lt.begin();
		pos++;

		lt.erase(pos);


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

	}

在这里插入图片描述
但是,需要注意的是,哨兵卫的头结点不可以被删除,因为一旦删除,那整个链表都无法找到
所以可以加一个断言
而且为了支持连续删除,我们还可以将返回值设为删除节点的下一个节点的指针

assert(pos != end());

头删尾删

void pop_front()
{
	erase(begin());
}

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

测试

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

		for (auto e : lt)
		{
			cout << e << " ";
		}
		cout << endl;
		//先尾删
		lt.pop_back();

		for (auto e : lt)
		{
			cout << e << " ";
		}
		cout << endl;
		//再头删
		lt.pop_front();

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

在这里插入图片描述

五. 析构函数

list的成员变量只有哨兵卫的头结点,要释放链表,我们需要先释放链表的节点,最后再释放哨兵卫的头结点

void clear()
{
	iterator it = begin();
	while (it != end())
	{
		//it = erase(it);
		erase(it++);
	}
}

~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

clear中,两种删除节点的方式都可以

第一种是删除当前节点,然后再用 it 接收下一个节点
第二种是复用后置++,返回的是要删除节点迭代器的拷贝,it迭代器其实已经指向下一个节点了

六. 构造函数

除了无参构造,STL的构造方式一般还有迭代器区间构造和拷贝构造

1. 迭代器区间构造

因为要适配各种数据类型的迭代器,不仅仅是内置类型,还有string,vector等的自定义类型。所以迭代器区间构造需要使用函数模板。

另外,再插入元素之前,我们还需要构造出哨兵卫的头结点。这时我们发现,不是只有无参构造需要构造哨兵卫的头结点,所以我们可以封装一个初始化的函数,并且在无参构造和迭代器区间构造复用

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

//无参构造
list()
{
	empty_init();
}

//迭代器区间构造
template<class Iterator>
list(Iterator first, Iterator last)
{
	empty_init();

	while (first != last)
	{
		push_back(*first);
		++first;
	}

}

测试

void list_test7()
	{
		list<int> lt1;
		lt1.push_back(1);
		lt1.push_back(2);
		lt1.push_back(3);
		lt1.push_back(4);

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

		list<int>lt2(lt1.begin(), lt1.end());

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

在这里插入图片描述

2. 拷贝构造

同vector的模拟实现,我们可以复用迭代器区间构造和swap函数,这样可以使得代码简洁一下,底层都是一个一个拷贝

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

//拷贝构造
list(const list<T>&lt)
{
	empty_init();

	//现代写法,复用迭代器区间构造和swap函数
	list<T>tmp(lt.begin(), lt.end());
	swap(tmp);
}

3. operator=

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

注意:此处传参不可以是&引用,因为swap会实现交换,如果用引用的话,原本的值会被改变,所以只能传参使用

七. 完整代码

//节点的结构体
	template<class T>
	struct list_node
	{
		list_node<T>*_next;
		list_node<T>*_prev;
		T _date;

		list_node(const T&x=T())
			:_next(nullptr)
			, _prev(nullptr)
			, _date(x)
		{}
	};

	template<class T,class Ref,class Ptr>
	struct _list_iterator
	{
		typedef list_node<T> node;
		typedef _list_iterator<T,Ref,Ptr> self;//因为需要返回迭代器,所以重命名一下 

		//成员变量
		node* _node;

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

		//operator*重载
		Ref&operator*()
		{
			return _node->_date;
		}

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

		//operator前置++重载
		self& operator ++()
		{
			_node = _node->_next;
			return *this;//返回迭代器本身
		}

		//operator后置++重载
		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;

			return tmp;
		}

		//operator前置--重载
		self& operator --()
		{
			_node = _node->_prev;
			return *this;//返回迭代器本身
		}

		//operator后置--重载
		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;

			return tmp;
		}

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

		//operator!=重载
		bool operator!=(const self&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;
		/*typedef const iterator const_iterator;*/


		iterator begin()
		{
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

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

		//迭代器区间构造
		template<class Iterator>
		list(Iterator first, Iterator last)
		{
			empty_init();

			while (first != last)
			{
				push_back(*first);
				++first;
			}

		}

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

		//拷贝构造
		list(const list<T>&lt)
		{
			empty_init();

			传统写法,一个一个拷贝
			//list<T>::const_iterator it = lt.begin();
			//while (it != lt.end())
			//{
			//	push_back(*it);
			//	++it;
			//}

			//现代写法,复用迭代器区间构造和swap函数
			list<T>tmp(lt.begin(), lt.end());
			swap(tmp);
		}

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

		void push_back(const T&date)
		{
			//node*tail = _head->_prev;
			//node*new_node = new node(date);

			断开旧尾和头的链接
			//tail->_next = new_node;
			//new_node->_prev = tail;
			链接新尾
			//new_node->_next = _head;
			//_head->_prev = new_node;

			//复用insert
			insert(end(), date);
		}
		
		void push_front(const T&date)
		{
			insert(begin(), date);
		}
		
		void insert(iterator pos, const T&x)
		{
			node*cur = pos._node;
			node*prev = cur->_prev;

			node*new_node = new node(x);

			prev->_next = new_node;
			new_node->_prev = prev;
			new_node->_next = cur;
			cur->_prev = new_node;
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			node*cur = pos._node;
			node*prev = cur->_prev;
			node*next = cur->_next;

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

			return iterator(next);
		}

		void pop_front()
		{
			erase(begin());
		}

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


		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
				erase(it++);
			}
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

	private:
		node* _head;
	};

结束语

本章list的模拟实现就先到这了,反向迭代器将单独出一篇博客,感谢阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值