[C++初阶]list的模拟实现

一、对于list的源码的部分分析

1.分析构造函数

首先,我们一开始最先看到的就是这个结点的结构体,在这里我们可以注意到这是一个双向链表。有一个前驱指针,一个后继指针。然后在有一个存储数据的空间

其次它的迭代器是一个自定义类型,而非原生指针。这里我们先不管,我们接着往下看。

我们继续找成员变量,在这里我们就找到了成员变量,但是这个类型我们看不懂,于是我们先略过。

通过对定义的查找,可以看到这个实际上还是一个指针。但是这个指针我们还是看不懂。

于是我们继续去速览定义,于是就找到了这里

这里我们可以注意到这个结点的类型其实就是一个类模板,这个模板正好就是我们一开始就看到的用结构体定义的一个结点。因此我们可以知道,这个成员变量实际就是一个指针,这个指针指向一个结点。这样一想象,就有点像我们在c语言使用链表时候的头节点了。

那么接下来,我们应该分析一下构造函数是什么样子的。

不难发现,就在成员变量的下方,正好就是一个无参的构造函数。也就是默认构造函数

但是在这里它又封装了一层函数,于是乎我们继续深入查看

如下所示,我们看到了具体的函数内容,从名字上来看,get_node 我们挺名字就知道开空间的。也就是得到一个结点,然后返回这个结点指针。这样一来,我们的成员变量就获取的实际的一个结点,然后让它的next和prev都指向自己,这样一来这个结点形成一个自循环。现在就能看出来这是一个带头双向循环链

我们也可以继续深入

如上图所示,这里的allocated设计到空间配置器。这里我们就先不做了解了。之后在学习中我们详细介绍。

我们往下看会看到put_node,这个函数其实就是释放结点的。在后面还有这样一个函数,这个函数是create_node不难理解,这个就是获取一个结点,先给这个结点开空间,然后调用构造函数。等一系列操作。既然这里涉及到一个构造,那么我们可以继续深入,看看这个构造里面是什么东西?但是这里涉及到了C++11的内容了,这边我们就先不管它了,我们只需要知道这里的空间能new出来就行。

2.对于头尾插的分析

作为链表,我们知道除了构造最重要的,肯定是头尾插,这里我们先找到头尾插的对应的函数

我们发现头尾插都调用了insert这个函数,因此我们需要先找到这个函数,

这里我们也是能大概理解的,先创造一个结点,然后进行连接。现在我们也基本读懂了这个大体的框架了,但是这里还需要注意的是:由于一开始的结点里面的指针都是空指针,导致后面需要经历很多的强制类型转化。所以这里我们如果一开始就定义好指针的类型是最好的。


二、list的模拟实现

1.list的节点声明

为了不和标准库中的list类冲突,我们可以开一个自己的命名空间,

其次,C++中对结点进行定义的时候可以只写类名,这与class是一样的。注意不要忘记带上模板参数T,因为我们写的结点也只是一个模板。因为类名不是类型,他实例化以后可以有各种各样的结点类型

cpp 复制代码
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;
	};

2.list类的成员变量

由于我们的结点是一个模板,对于它而言,它的类型就比较繁琐,我们可以在list类里面使用typedef进行一次重命名。然后再私有里面再定义一个指针,这个指针就是一个结点指针。

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

	private:
		Node* _head;
	};

3.list类的默认构造函数

如下所示,我们定义好了成员变量以后,我们就写一个默认构造函数,当我们对这个链表类进行实例化的时候,自动调用这个默认构造函数,这个默认构造函数会为成员变量的头节点指针分配实际的空间,在new Node空间的时候,会调用它Node(即list_node<T>)类的默认构造函数函数。从而成功的开辟好这块空间。

cpp 复制代码
		list()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}

4.list类的尾插

如下所示,我们的尾插逻辑也是比较简单的,先利用我们传过来的val去开辟一个新的结点,注意这里开辟新结点的时候使用new的话可以直接带一个括号去调用它的构造函数

cpp 复制代码
		void push_back(const T& val)
		{
			Node* newnode = new Node(val);
			Node* tail = _head->_prev;

			tail->_next = newnode;
			newnode->_prev = tail;

			newnode->_next = _head;
			_head->_prev = newnode;
		}

5.结点的默认构造函数

有了上面的分析之后,我们现在缺的是一个结点的默认构造函数,我们直接给一个缺省值,使用T()就是一个匿名对象来初始化,对于内置类型也是同样适用的。然后我们使用初始化列表即可。

cpp 复制代码
		list_node(const T& val = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_val(val)
		{}

6.list类的迭代器

首先我们思考一下,可否像vector一样直接在类里面typedef 一个迭代器?

显然是不行的,因为链表不支持下标随机访问,list是不连续的,指针加1后,地址早已不知道跑到哪个结点去了。其次这里仅仅只是结点的指针,解引用后,找到的仅仅只是结点,我们还需要进一步解引用才能找到对应的真正的值。

直接typedef的话,会使得迭代器的++和解引用操作均失效了,这时候我们只能使用运算符重载了。才能解决这个问题。既然要运算符重载,这里我们最好再次封装一个类出来。因为如果不封装一个类出来的话,我们无法完成此处的运算符重载。

如下所示,是我们实现的iterator的类

cpp 复制代码
	template<class T>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		Node* _node;

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

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

		__list_iterator<T>& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		bool operator!=(const __list_iterator<T>& it)
		{
			return _node != it._node;
		}

	};

这个类我们使用了一个结构体去封装,在这个结构体中,我们只有一个成员变量,这个成员变量是结点类的指针,用于指向某一个结点, 在我们一开始定义出迭代器的时候,我们需要先写一个构造函数,用于迭代器的初始化。即需要传一个参数node来控制。

与之对应的,我们在list中就需要写出对应的begin和end函数,来返回迭代器。

cpp 复制代码
		typedef __list_iterator<T> iterator;
		iterator begin()
		{
			//return _head->_next //单参数的构造函数支持隐式类型转换
			return iterator(_head->_next);
		}

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

下面我们这里将迭代器的基本操作写的稍微完善一些

cpp 复制代码
	template<class T>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		Node* _node;

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

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

		__list_iterator<T>& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		__list_iterator<T> operator++(int)
		{
			__list_iterator<T> tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		bool operator!=(const __list_iterator<T>& it)
		{
			return _node != it._node;
		}

		bool operator==(const __list_iterator<T>& it)
		{
			return _node == it._node;
		}
	};

7.const迭代器

我们可以拷贝一份原来的迭代器,然后改变一下类名和解引用这个成员函数的返回值即可

cpp 复制代码
	template<class T>
	struct __list_const_iterator
	{
		typedef list_node<T> Node;
		Node* _node;

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

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

		__list_const_iterator<T>& operator++() 
		{
			_node = _node->_next;
			return *this;
		}

		__list_const_iterator<T> operator++(int) 
		{
			__list_const_iterator<T> tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		bool operator!=(const __list_const_iterator<T>& it) 
		{
			return _node != it._node;
		}

		bool operator==(const __list_const_iterator<T>& it) 
		{
			return _node == it._node;
		}
	};

即如上代码所示,但是这样设计太过于冗余了。不过这个也是可以正常运行的,我们先补两个接口

cpp 复制代码
		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

		const_iterator end() const
		{
			return const_iterator(_head);
		}

其实我们可以通过一个类型去控制这个返回值,而要控制这个类型,就需要增加一个模板参数即可

在迭代器类中添加一个Ref参数,然后让*的运算符重载返回这个模板参数,最后代码如下:

cpp 复制代码
	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)
		{}

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

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

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		bool operator!=(const self & it)
		{
			return _node != it._node;
		}

		bool operator==(const self & it)
		{
			return _node == it._node;
		}
	};

当然现在还没完,还有时候,我们可能会写出这样的代码

cpp 复制代码
	struct A
	{
		A(int a = 0, int b = 0)
			:_a(a)
			,_b(b)
		{}
		int _a;
		int _b;
	};
	void test2()
	{
		list<A> lt;
		lt.push_back(A(1, 1));
		lt.push_back(A(2, 2));
		lt.push_back(A(3, 3));
		lt.push_back(A(4, 4));
		lt.push_back(A(5, 5));


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

我们显然发现是无法正常运行的。由于迭代器是类似于指针的操作,我们有时候就需要->操作符去解引用。所以,我们需要加上一个->的运算符重载。

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

如上所示,是写在迭代器里面的operator运算符重载,

然而当我们细心的话,我们会发现这个运算符重载是比较怪异的。哪里怪异呢?

首先我们这个运算符重载返回的是什么呢?是A*,也就是说它还需要一次->解引用才能找到真正的值。那么我们这里为什么可行呢?

严格来说,it->->_a,才是符合语法的。

因为运算符重载要求可读性,那么编译器特殊处理,省略了一个->

上面这个运算符重载仅仅只是针对于普通对象的,如果是const对象的话,那么我们只能使用跟*运算符重载一样的处理方法,多传一个参数,才可以解决这个问题。也就是我们需要三个模板参数才可以。

最终我们的迭代器代码如下所示

cpp 复制代码
	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* node)
			:_node(node)
		{}

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

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

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

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		
		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		bool operator!=(const self & it) const
		{
			return _node != it._node;
		}

		bool operator==(const self & it) const
		{
			return _node == it._node;
		}
	};

8.插入和删除接口

整体简单,这里直接展示源码:

cpp 复制代码
		void push_back(const T& val)
		{

			//insert(end(), val);
			Node* newnode = new Node(val);
			Node* tail = _head->_prev;

			tail->_next = newnode;
			newnode->_prev = tail;

			newnode->_next = _head;
			_head->_prev = newnode;
		}
		void push_front(const T& val)
		{
			insert(begin(), val);
		}

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

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

		iterator insert(iterator pos, const T& val)
		{
			Node* newnode = new Node(val);
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			prev->_next = newnode;
			newnode->_prev = prev;

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

			return newnode;
		}

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

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

			prev->_next = next;
			next->_prev = prev;

			return next;
		}

9.size

这个不难,我可以通过遍历解决得出size,但是这样子的时间复杂度可能有点高,

cpp 复制代码
		size_t size()
		{
			size_t sz = 0;
			iterator it = begin();
			while (it != end())
			{
				it++;
				sz++;
			}
			return sz;
		}

这里我们也可以用第二种方法,多加一个成员变量存储节点数量,每次插入节点时++,删除节点时--,这里我就不多做解释了

10.clear

这个就是遍历释放空间就行

cpp 复制代码
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

11.析构函数

析构函数也是很简单的,析构和clear的区别就是析构会删除头节点,而clear不会删除头节点。

cpp 复制代码
		~list()
		{
			clear();

			delete _head;
			_head = nullptr;
		}

12.拷贝构造函数

这里设计空间的开辟,所以需要深拷贝,代码如下:

cpp 复制代码
		list(const list<T>& lt)
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;

			for (auto& e : lt)
			{
				push_back(e);
			}
		}

当然在这里我们发现拷贝构造和默认构造有点重复了。我们可以对前面重复的部分在封装一个函数,从而简化代码:

cpp 复制代码
		void empty_init()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

		list()
		{
			//_head = new Node;
			//_head->_next = _head;
			//_head->_prev = _head;
			//_size = 0;
			empty_init();
		}

		list(const list<T>& lt)
		{
			//_head = new Node;
			//_head->_next = _head;
			//_head->_prev = _head;
			//_size = 0;
			empty_init();

			for (auto& e : lt)
			{
				push_back(e);
			}
		}

13.赋值运算符重载

如下所示,这个也是比较简单,我们直接使用现代写法吧:

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类的全部代码

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<list>
#include<assert.h>

using namespace std;

namespace Sim
{
	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)
		{}
	};

	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* node)
			:_node(node)
		{}

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

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

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

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

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

		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		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 list_node<T> Node;
	public:

		typedef __list_iterator<T, T&, T*> iterator;
		//typedef __list_const_iterator<T> const_iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;
		iterator begin()
		{
			//return _head->_next //单参数的构造函数支持隐式类型转换
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}
		const_iterator begin() const
		{
			//return _head->_next //单参数的构造函数支持隐式类型转换
			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;
			_size = 0;
		}

		list()
		{
			//_head = new Node;
			//_head->_next = _head;
			//_head->_prev = _head;
			//_size = 0;
			empty_init();
		}

		list(const list<T>& lt)
		{
			//_head = new Node;
			//_head->_next = _head;
			//_head->_prev = _head;
			//_size = 0;
			empty_init();

			for (auto& e : lt)
			{
				push_back(e);
			}
		}
		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;
		}

		void push_back(const T& val)
		{

			insert(end(), val);
			//Node* newnode = new Node(val);
			//Node* tail = _head->_prev;

			//tail->_next = newnode;
			//newnode->_prev = tail;

			//newnode->_next = _head;
			//_head->_prev = newnode;
		}
		void push_front(const T& val)
		{
			insert(begin(), val);
		}

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

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

		iterator insert(iterator pos, const T& val)
		{
			Node* newnode = new Node(val);
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			prev->_next = newnode;
			newnode->_prev = prev;

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

			++_size;

			return newnode;
		}

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

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

			prev->_next = next;
			next->_prev = prev;

			--_size;

			return next;
		}

		size_t size()
		{
			//size_t sz = 0;
			//iterator it = begin();
			//while (it != end())
			//{
			//	it++;
			//	sz++;
			//}
			//return sz;
			return _size;
		}

		~list()
		{
			clear();

			delete _head;
			_head = nullptr;
		}

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

	private:
		Node* _head;
		size_t _size;
	};

	}
}
相关推荐
敢敢J的憨憨L4 分钟前
Linux C/C++ 库链接选项 --whole-archive,--no-whole-archive和--start-group, --end-group
linux·c语言·c++·库链接选项
XY_墨莲伊4 分钟前
【单片机原理及应用】实验: 8位数码显示器
开发语言·笔记·单片机·嵌入式硬件·中间件·计算机外设·硬件架构
跃ZHD15 分钟前
java基础之枚举Enum
java·开发语言
dawn19122816 分钟前
【CSP:202012-2】期末预测之最佳阈值(Java)
java·开发语言·算法
睡觉然后上课16 分钟前
学构造函数的这辈子有了
开发语言·c++·qt
CCTV果冻爽24 分钟前
LuaJit分析(四)luajit 64位与32位字节码区别
java·开发语言
奶芙c25 分钟前
【C++】C++11
数据结构·c++·算法
冯大少28 分钟前
Python 高级特效 - 生成器 ( Generator)
开发语言·python
SunkingYang33 分钟前
boost库容器之Circular Buffer功能介绍,及使用示例
c++·boost·用例·circular buffer·循环缓冲区·功能介绍
让开,我要吃人了34 分钟前
HarmonyOS开发实战( Beta5版)优化实践/合理使用缓存提升性能
linux·开发语言·缓存·华为·移动开发·harmonyos·鸿蒙