list的模拟实现

本文深入探讨了C++ STL中的list容器实现,包括节点结构、双向迭代器的详细操作,如解引用、“->”访问、前后移动以及比较。此外,还介绍了list的构造函数、拷贝构造、赋值重载、增删查改等核心操作,如push_back、push_front、insert、erase和clear。通过对底层结构和迭代器的解析,有助于理解list的工作原理。
摘要由CSDN通过智能技术生成


在上一篇博客中讲解了list的用法,了解了list的基本使用。这篇博客将会详细介绍list的底层实现原理,更加深入的了解list。

list的节点类型

list的底层是一个双向链表,因此list的节点中包括一前一后两个指针,以及节点的值val。指针prev指向该节点的前一个节点,指针next指向该节点的后一个节点。

//定义节点,通过模板来定义
template <class T>
//用struct定义成公有的,再list类中可以直接访问
struct List_Node
{
	T _val;
	List_Node<T>* _prev;
	List_Node<T>* _next;

	//这里的节点也相当于一个类,因此需要写一个构造函数,针对那些开辟空间时调用的初始化
	List_Node(const T val = T())//构造匿名对象进行赋值
		: _prev(nullptr)
		, _next(nullptr)
		, _val(val)
	{}
};

为了能够使节点类型能够满足各种类型,因此节点的定义采用模板实现。并且将其定义为struct,方便list类在类外直接访问节点。

list的迭代器

前几篇博客中讨论的容器,比如string、vector等等的迭代器都是使用的原生指针,这是因为string、vector等容器底层空间是连续的,原生指针就具有迭代器的性质。
但是list每一个节点在底层空间中不连续,因此原生指针作为迭代器就不合适。因此list的迭代器实现就必须对原生指针进行封装,使得迭代器的使用形式与指针完全相同。

解引用

指针是可以解引用来获取指针对应的值,在list的迭代器中必须重载operator*。
在list重载operator也就是获取每个节点对应的值val,因此在list的迭代器中重载operator就是通过每一个节点的指针取到节点对应的值。

//重载*运算符
Ref operator*()
{
	return _node->_val;
}

通过“->”访问成员

指针可以通过“->”访问其成员,这也是指针的特性之一。在list中想要完成“->”的重载,可以通过对operator*()进行取地址得到,使得重载后的“->”拿到节点的地址对节点成员进行访问。

//这里重载->是为了给自定义类型去使用,使得自定义类型能够通过指针来访问空间变量
Ptr operator->()
{
	return &(operator*());
}

指针的前后移动

由于list的节点在底层空间中是不连续的,因此普通的++和–无法完成list中的前进和后退,必须在迭代器中对++和–进行重载。
++的操作通过节点中的next指针来完成;–的操作通过节点中的prev来完成。

//前置++和后置++的区别:
//1、前置++的参数中只有this指针,后置++的参数除了this指针,还有int进行占位
//2、前置++先修改再使用,所以返回值加引用;后置++先使用再修改,返回值没有引用。

//重载后置++运算符
self operator++(int)
{
	_node = _node->_next;
	return *this;
}

//重载前置++
self& operator++()
{
	_node = _node->_next;
	return *this;
}

//重载后置--运算符
self operator--(int)
{
	_node = _node->_prev;
	return *this;
}

//重载前置--
self& operator--()
{
	_node = _node->_prev;
	return *this;
}

相等的比较

由于迭代器还需要进行是否相等的比较,因此迭代器还必须对operator==和operator!=进行重载。
两个迭代器之间的比较就是比较两个节点的地址是否相同,那么在底层可以直接通过比较节点是否相等来完成。

//重载!=
bool operator!=(self& l)//比较的时候是同一类的对象,参数应该也是对象,不应该是节点
{
	return _node != l._node;
}

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

迭代器部分的完整代码如下:

	//如果想实现const迭代器,如果重新写一个相似的类就会出现代码冗余,因此可以在模板参数这里做调整
	//给定三个模板参数:T、Ref、Ptr
	//当参数不同时,对象的类型也就不同
	template <class T, class Ref, class Ptr>
	struct List_Iterator
	{
		typedef List_Node<T> Node;
		typedef Node* pNode;
		typedef List_Iterator<T, Ref, Ptr> self;
	public:
		List_Iterator(pNode node = nullptr)//迭代器的构造函数就是根据某个节点创建的
			: _node(node)
		{}

		//重载*运算符
		Ref operator*()
		{
			return _node->_val;
		}

		//这里重载->是为了给自定义类型去使用,使得自定义类型能够通过指针来访问空间变量
		Ptr operator->()
		{
			return &(operator*());
		}

		//针对const对象,这里不能在重载*这里加上const
		//因为对象是const,所以返回的迭代器也应该是const迭代器
		//const T& operator*() const
		//{
		//	return _node->_val;
		//}

		//前置++和后置++的区别:
		//1、前置++的参数中只有this指针,后置++的参数除了this指针,还有int进行占位
		//2、前置++先修改再使用,所以返回值加引用;后置++先使用再修改,返回值没有引用。

		//重载后置++运算符
		self operator++(int)
		{
			_node = _node->_next;
			return *this;
		}

		//重载前置++
		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		//重载后置--运算符
		self operator--(int)
		{
			_node = _node->_prev;
			return *this;
		}

		//重载前置--
		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		//重载!=
		bool operator!=(self& l)//比较的时候是同一类的对象,参数应该也是对象,不应该是节点
		{
			return _node != l._node;
		}

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

		pNode _node;//迭代器中节点是基本的成员变量
	};

这其中有一点需要注意:
迭代器一般分为非const迭代器和const迭代器,如果是原生指针的画,直接加const就可以完成。但是如果是封装的迭代器,要完成const迭代器,一种方法是写一份相似的迭代器封装,在某些位置加上const,但是这样做会出现代码冗余的情况;第二种方法就是在模板参数上做文章,在模板参数传三个参数,分别对应list的值、引用和指针,这样子我们如果想是用const类型的迭代器,直接传const类型的模板参数即可。

list的构造函数

头节点构造

在实现list的构造函数之前,首先需要一个头节点构造的函数,用于对头节点进行创建。

//头节点创建
void CreateHead()
{
	_head = new Node;
	_head->_prev = _head;
	_head->_next = _head;
}

基本构造函数

list中,基本的构造函数就是只创建一个头节点。

//基本构造
list()
{
	CreateHead();
}

构造n个节点

n个节点的构造函数,在创建头节点之后,通过循环将节点尾插在头节点之后。

list(size_t n, const T& val = T())
{
	CreateHead();
	while (n--)
		push_back(val);
}

这里val的缺省通过一个匿名对象来给定。

通过迭代器构造

由于迭代器根据容器的底层实现是不同的,因此通过迭代器构造list必须以模板来实现,这样才能实现对于各种迭代器的构造。这里的构造就是从前到后遍历迭代器,对每一个节点进行尾插。

//利用迭代器构造
template <class InputIterator>
list(InputIterator first, InputIterator last)
{
	CreateHead();
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

拷贝构造

对于拷贝构造,这里有两种方法:
第一种是定义一个头节点,将被拷贝对象中的每一个元素在新的对象中进行尾插。
第二种是通过构造一个临时对象,与新对象进行交换即可。

//拷贝构造函数
list(const list<T>& x)
{
	//CreateHead();
	//第一种方法:自己定义一个头节点,将x中的每个元素尾插
	//const_iterator it = x.begin();
	//while (it != x.end())
	//{
	//	push_back(*it);
	//	it++;
	//}

	//第二种方法:定义一个临时对象,进行交换
	list<T> tmp(x.cbegin(), x.cend());
	swap(tmp);
}

比较建议写第二种,简单易懂。

重载operator=()

重载赋值运算符与拷贝构造一样,也有两种方法:
第一种是将赋值对象的每一个元素进行尾插;
第二种是在参数中赋值对象不加引用,由于此时传过来的是拷贝构造的临时对象,与当前对象进行交换即可。

//重载=运算符
//赋值的话,没有定义过的是调拷贝构造,定义过的是赋值重载
list<T>& operator=(const list<T> x)
{
	//第一种方法:就是将x的每个元素尾插
	//第二种方法:定义一个临时对象,将头节点交换
	swap(tmp);
	return *this;
}

注意:赋值时,如果被赋值的对象是未定义的,此时调用的是拷贝构造;定义过的调用的是赋值重载。

list的增删查改

尾插

//push_back函数
void push_back(const T& val)
{
	pNode newnode = new Node(val);//这里可以认为是根据节点的类调用构造函数
	pNode tail = _head->_prev;
	tail->_next = newnode;
	newnode->_prev = tail;
	newnode->_next = _head;
	_head->_prev = newnode;
}

头插

//push_front函数
void push_front(const T& val)
{
	pNode newnode = new Node(val);
	pNode first = _head->_next;
	// _head     newnode    first
	newnode->_prev = _head;
	_head->_next = newnode;
	newnode->_next = first;
	first->_prev = newnode;
}

insert函数

//1、普通插入,在pos位置之前
void insert(iterator pos, const T& val)
{
	assert(pos._node);//断言以下空指针
	pNode newnode = new Node(val);
	pNode cur = pos._node;
	pNode prev = cur->_prev;
	newnode->_prev = prev;
	prev->_next = newnode;
	newnode->_next = cur;
	cur->_prev = newnode;
}

//2、填充n个值
void insert(pNode pos, size_t n, const T& val)
{
	assert(pos);
	while (n--)
	{
		insert(pos, val);//复用前面的insert
		pos = pos->_prev;
	}
}

尾删

//pop_back函数
void pop_back()
{
	assert(_head != _head->_next);
	pNode tail = _head->_prev;
	pNode last = tail->_prev;
	delete tail;
	last->_next = _head;
	_head->_prev = last;
}

头删

//pop_front函数
void pop_front()
{
	assert(_head != _head->_next);
	pNode first = _head->_next;
	pNode newfirst = first->_next;
	delete first;
	_head->_next = newfirst;
	newfirst->_prev = _head;
}

erase函数

//erase函数
pNode erase(pNode pos)
{
	assert(_head != _head->_next);
	pNode prev = pos->_prev;
	pNode next = pos->_next;
	delete pos;
	prev->_next = next;
	next->_prev = prev;
	return next;
}

clear函数

//clear函数
void clear()
{
	//如果只有一个头节点,不需要动
	assert(_head->_next == _head);
	pNode cur = _head->_next;
	while (cur != _head)
	{
		pNode next = cur->_next;
		pop_back();//复用尾删
		cur = next;
	}
}

析构函数

//析构函数
~list()
{
	pNode cur = _head->_next;
	while (cur != _head)
	{
		pNode next = cur->_next;
		delete cur;
		cur = next;
	}
	delete _head;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值