STL详解——list的模拟实现

文章目录

一、需要实现的三个类模板

二、list_node类模板的完善

三、迭代器模板的完整实现以及思路

解引用的重载

->运算符的重载

前置++的重载

前置--的重载

!=以及==的重载

四、list类模板的完善

默认成员函数的实现

无参构造

拷贝构造实现

赋值运算符重载函数

析构函数

迭代器相关函数

访问容器相关函数

front和back

插入、删除函数

erase

push_back和pop_back

push_front和pop_front

其他函数

size

empty

clear


一、需要实现的三个类模板

通过观察源码,我们能发现,要实现list我们需要设计三个类,主要框架如下:

cpp 复制代码
namespace mine
{
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;
	};
	template<class T, class Ref, class Ptr>
	struct list_iterator
	{
		typedef list_node<T> Node;
		typedef list_iterator<T, Ref, Ptr> Self;//     引用 指针	


		Node* _node;
	};
	template<class T>
	class list
	{
	public:
		typedef list_node<T> Node;

	private:
		Node* _head;
		int _size = 0;
	};
}

关于list以及list_node 这两个类模板的设计我想我们应该很容易理解,但是我们为什么需要实现迭代器模板?我们之前实现string以及vector时,我们只是使用typedef利用对应类型的指针实现迭代器的功能。其实究其原因还是因为存储机制不同,string以及vector对象中的元素存储在一片连续的内存空间,我们利用指针的变换就可以实现迭代器的功能,但是list的各个节点的地址不是连续的,利用指针变换得到的不会是我们想要的节点,那我们怎么实现对应的功能呢?不要忘记,节点同样存储了指向前一个节点以及后一个节点的指针,我们利用这两个指针就可以实现迭代器的功能,我们后面会根据上述模板逐步实现对应的功能,其实list模板类我们只会选择常用的函数接口

二、list_node类模板的完善

我们只需要写一个带参构造就好了

cpp 复制代码
template<class T>
struct list_node//成员默认为公有,便于后面的访问
//存在其他方法,可以利用友元或者内部类解决,但是我们观察源码发现是分别封装的,所以我们也分开来写
{
	list_node(const T& data=T())//若构造结点时未传入数据,则默认以list容器所存储类型的默认构造函数所构造出来的值为传入数据。
		:_data(data),_next(nullptr),_prev(nullptr)
	{ }
	//成员变量
	T _data;
	//指针域
	list_node<T>* _next;//前驱指针
	list_node<T>* _prev;//后继指针
};

三、迭代器模板的完整实现以及思路

我们已经能给出了模板的大致框架,但是我们看到会感到很疑惑?这是什么我怎么看不懂?因为这是我们二次优化后的代码,我们给出这个接口应该就可以看懂了

cpp 复制代码
template<class T>
struct list_iterator  //普通迭代器的实现
{
    list_iterator(Node* node)
	:_node(node)
    {}
	typedef list_node<T> Node;
	typedef list_iterator<T> Self;
	//成员变量
	Node* _node;
};

那我们先完善一下这个模板的功能:

我们常使用迭代器进行对象的遍历,那我们就需要实现 如下接口:

cpp 复制代码
T& operator*();
T* operator->();
Self& operator++();
Self& operator--();
bool operator!=(const Self& s) const;
bool operator==(const Self& s) const;

解引用的重载

对于第一个,我们对迭代器进行解引=引用主要是为了获取迭代器对应节点的数据,那么很容易我们就能实现:

cpp 复制代码
T& operator*()
{
	return  _node->_data;
}

对于第二个,可能会存在如下的场景当list容器当中的每个结点存储的不是内置类型,而是自定义类型,例如日期类,那么当我们拿到一个位置的迭代器时,我们可能会使用->运算符访问Date的成员:

cpp 复制代码
	list<Date> lt;
	Date d1(2021, 8, 10);
	Date d2(1980, 4, 3);
	Date d3(1931, 6, 29);
	lt.push_back(d1);
	lt.push_back(d2);
	lt.push_back(d3);
	list<Date>::iterator pos = lt.begin();
	cout << pos->_year << endl; //输出第一个日期的年份

->运算符的重载

我们直接返回结点当中所存储数据的地址即可。

cpp 复制代码
T* operator->()
{
	return &_node->_data;
}

讲到这里,可能你会觉得不对,按照这种重载方式的话,这里使用迭代器访问日期类当中的成员变量时不是应该用两个->吗?

这里本来是应该有两个->的,第一个箭头是pos ->去调用重载的operator->返回Date* 的指针,第二个箭头是Date* 的指针去访问对象当中的成员变量_year。但是一个地方出现两个箭头,程序的可读性太差了,所以编译器做了特殊识别处理,为了增加程序的可读性,省略了一个箭头。

前置++的重载

前置++原本的作用是将数据自增,然后返回自增后的数据。我们的目的是让结点指针的行为看起来更像普通指针,那么对于结点指针的前置++,我们就应该先让结点指针指向后一个结点,然后再返回"自增"后的结点指针即可。

cpp 复制代码
Self& operator++()
{
	_node = _node->_next;
	return *this;
}

前置--的重载

cpp 复制代码
Self& operator--()
{
	_node = _node->_prev;
	return *this;
}

!=以及==的重载

cpp 复制代码
bool operator!=(const Self& s) const
{
	return _node != s._node;
}	

bool operator==(const Self& s) const
{
		return _node == s._node;
}

就是判断两个节点的数据是否相同,应该是很容易理解并实现了。

思考:如果我们要实现const_iterator迭代器应该怎么实现?

cpp 复制代码
template<class T>
struct list_const_iterator  //const迭代器的实现
{
	typedef list_node<T> Node;
	typedef list_const_iterator<T> Self;
	list_iterator(Node* node)
		:_node(node)
	{}

	const T& operator*()
	{
		return  _node->_data;
	}

	const T* operator->()
	{
		return &_node->_data;
	}

	const Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	const Self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	bool operator!=(const Self& s) const
	{
		return _node != s._node;
	}

	bool operator==(const Self& s) const
	{
		return _node == s._node;
	}

	//成员变量
	Node* _node;
};

我们对比一下发现其实就是对部分接口进行了一些修改,利用const进行了修饰,那对于我们的程序来说就会导致有些笼余以及臃肿,那么我们应该怎么实现?想不出来,我们看看源码,那些大佬是怎么实现的,其实就最前面我们给出的接口:

cpp 复制代码
struct list_iterator
	{
		typedef list_node<T> Node;
		typedef list_iterator<T, Ref, Ptr> Self;//     引用 指针	


		Node* _node;
	};

意思是在该类模板中可以使用三种不同类型的数据类型,这有什么用?能为我们解决问题吗?

我们观察上面实现的代码,可以发现会使用 T T& T*这三种类型,恰恰对应着源码接口的三种类型。

具体功能的实现我们已经在上文中进行了详细的讲解,那下面我们直接给出迭代器最终版本的代码:

cpp 复制代码
	template<class T, class Ref, class Ptr>
	struct list_iterator
	{
		typedef list_node<T> Node;
		typedef list_iterator<T, Ref, Ptr> Self;//引用 指针

		list_iterator(Node* node)
			:_node(node)
		{}

		Ref operator*()
		{
			return _node->_data;
		}

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

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

		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		Self operator++(int)//后置++,加int形参
		{
			Self tmp(*this);
			_node = _node->_next;

			return tmp;
		}

		Self& operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;

			return tmp;
		}

		bool operator!=(const Self& s) const
		{
			return _node != s._node;
		}

		bool operator==(const Self& s) const
		{
			return _node == s._node;
		}

		Node* _node;
	};

其实本质上我们是将第一种类模板进行了更深层次的细化,得到的结果本质上还是类模板最终都要实例化成对象。

当我们使用普通迭代器时,编译器就会实例化出一个普通迭代器对象;当我们使用const迭代器时,编译器就会实例化出一个const迭代器对象。若该迭代器类不设计三个模板参数,那么就不能很好的区分普通迭代器和const迭代器。

四、list类模板的完善

cpp 复制代码
class list
{
public:
	typedef list_node<T> Node;

	typedef list_iterator<T, T&, T*> iterator;
	typedef list_iterator<T, const T&, const T*> const_iterator;
private:
	Node* _head;
	int _size;
};

我们将在此基础上进行功能的完善。

默认成员函数的实现

无参构造
cpp 复制代码
list()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
	_size = 0;
}

我们需要为头结点开辟一个空间,然后构建成双向循环链表,就是让头节点的指向前一个节点以及后一个节点的指针指向自己。

拷贝构造实现
cpp 复制代码
list(const list<T>& lt)
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
	_size = 0;
	auto it = lt.begin();
	while (it != lt.end())
	{
		push_back(*it);
		it++;
	}
}

我们可以想成先利用无参构造创建一个list,然后将母本中的元素尾插进该表中。

赋值运算符重载函数

传统写法:

cpp 复制代码
list<T>& operator= (const list& lt)
{
	if (&lt != this)
	{
		clear();
		auto it = lt.begin();
		while (it != lt.end())
		{
			push_back(*it);
			it++;
		}
	}
	return *this;
}

我们首先要保证母本不是他本身,否则会造成混乱。传统写法我们看代码应该很容易理解,那么我们看看现代写法:

cpp 复制代码
void swap(list<T>& lt)
{
	std::swap(_head, lt._head); //交换两个容器当中的头指针
	std::swap(_size, lt._size);
}
list<T>& operator=(list<T> lt) //编译器接收右值的时候自动调用其拷贝构造函数
{
	swap(lt); //交换这两个对象
	return *this; //支持连续赋值
}

首先利用编译器机制,故意不使用引用接收参数,通过编译器自动调用list的拷贝构造函数构造出来一个list对象,然后调用swap函数将原容器与该list对象进行交换即可。

析构函数
cpp 复制代码
~list()
{
	clear();
	delete _head;
}

我们先利用clear函数清楚表中所有的有效节点,只保留头结点,最后利用delete销毁头结点。

迭代器相关函数

cpp 复制代码
iterator begin()
{
	return _head->_next;
}
const_iterator begin() const
{
	return _head->_next;
}
iterator end()//最后一个元素之后
{
	return _head;
}
const_iterator end() const
{
	return _head;
}

其中我们最需要注意的是end返回的是最后一个有效节点的下一个位置,由于编译器会自动调用构造,所以我们直接返回节点是没有问题的。

访问容器相关函数

front和back

front和back函数分别用于获取第一个有效数据和最后一个有效数据,因此,实现front和back函数时,直接返回第一个有效数据和最后一个有效数据的引用即可。

cpp 复制代码
		T& front()
		{
			assert(_size!=0);//链表不能只有一个哨兵位的头结点
			return _head->_next->_data;
		}
		const T& front() const
		{
			assert(_size != 0);
			return _head->_next->_data;
		}

		T& back()
		{
			assert(_size != 0);
			return _head->_prev->_data;
		}
		const T& back() const
		{
			assert(_size != 0);
			return _head->_prev->_data;
		}

插入、删除函数

insert可以在指定位置之前插入确值的节点:

cpp 复制代码
iterator insert(iterator pos, const T& val)
{
	Node* newnode = new Node(val);//我们首先需要创建一个新的节点
	Node* cur = pos._node;//存储
	
	newnode->_next = cur;
	newnode->_prev = cur->_prev;

	cur->_prev->_next = newnode;
	cur->_prev = newnode;
	++_size;

	return iterator(newnode);
}

我们首先需要创建一个新的节点,再指明目标位置的节点,进行操作。

erase

erase函数可以删除所给迭代器位置的结点。

先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,以及后一个位置的结点指针next,紧接着释放cur结点,最后建立prev和next之间的双向关系即可。

cpp 复制代码
iterator erase(iterator pos)//返回抹除元素的后一个
{
	assert(pos != end());

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

	prev->_next = next;
	next->_prev = prev;
	delete pos._node;

	--_size;

	return next;
}
push_back和pop_back

push_back和pop_back函数分别用于list的尾插和尾删,在已经实现了insert和erase函数的情况下,我们可以通过复用函数来实现push_back和pop_back函数。

push_back函数就是在头结点前插入结点,而pop_back就是删除头结点的前一个结点。

cpp 复制代码
void push_back(const T& val)
{
	insert(iterator(_head), val);
}
void pop_back()
{
	erase(iterator(_head->prev));
}
push_front和pop_front

当然,用于头插和头删的push_front和pop_front函数也可以复用insert和erase函数来实现。

push_front函数就是在第一个有效结点前插入结点,而pop_front就是删除第一个有效结点。

cpp 复制代码
void push_front(const T& val)
{
	insert(begin(), val);
}
void pop_front()
{
	erase(begin());
}

其他函数

size

我们为了减少遍历,所以我们直接创建了_size成员变量,当我们需要时,直接通过size()函数返回该成员变量的值。

cpp 复制代码
size_t size() const
{
	return _size;
}
empty

判空函数,我们判断size是否为0即可。

cpp 复制代码
bool empty() const
{
	return _size == 0;
}
clear

clear函数用于清空容器,我们通过遍历的方式,逐个删除结点,只保留头结点即可。

cpp 复制代码
void clear()
{
	if (_size == 0)
		return;
	auto it = begin();
	while (it != end())
	{
		it=erase(it);
	}
}
相关推荐
雪度娃娃1 小时前
行为型设计模式——命令模式
c++·设计模式·命令模式
司晨卿1 小时前
claude windows安装
windows
我能坚持多久1 小时前
STL详解——list的介绍以及功能展示
开发语言·c++
大大杰哥1 小时前
2026陕西省ICPC省赛补题(前六题)
c++·算法
我是Superman丶1 小时前
Windows 创建软链接/目录联接命令
windows
Brilliantwxx1 小时前
【C++】 继承与多态(上)
开发语言·c++·笔记·算法
不负岁月无痕1 小时前
STL -- C++ string 类 模拟实现
java·开发语言·c++
·心猿意码·1 小时前
OCCT源码解析(六):TKG3d 模块——三维曲面体系
c++·3d
会开花的二叉树2 小时前
Qt初体验-第一个窗口程序踩的坑
开发语言·c++·qt