【项目分享-知识讲解】 C++标准库 list类的模拟实现


Gitee仓库:

拂拉氏/my_list


目录

前言

[Part1. 基本架构](#Part1. 基本架构)

[Part2. list_node的实现](#Part2. list_node的实现)

[Part3. list_iterator的实现](#Part3. list_iterator的实现)

[Part3.1. list_iterator中"->"的重载](#Part3.1. list_iterator中“->”的重载)

[Part3.2. const_iterator的适配](#Part3.2. const_iterator的适配)

[Part4. 基于list_node和list_iterator的list实现](#Part4. 基于list_node和list_iterator的list实现)

[Part4.1. 封装思想在list体现](#Part4.1. 封装思想在list体现)

[Part4.2. 迭代器失效](#Part4.2. 迭代器失效)

[Part5. 其他函数](#Part5. 其他函数)

[Part6. 结语](#Part6. 结语)


前言

C++标准库list类作为STL的重要组成部分,我们有必要去了解它,而了解它最好的方式就是亲自去实现一波。接下来,让我们来看一下吧。


let's go!!!!!!!


Part1. 基本架构

首先,我们要先明确我们要实现的东西的一个基本框架。我们主要有三种结构要实现:


1. list :向外暴露的容器,只存储哨兵位节点,通过这个来对整个链表统一管理。


2. list_node :具体的链表的每个节点,包括存数据的data,前驱节点prev,后驱节点next。有利于我们支持泛型编程(即对一套代码可以实现对不同类型的数据使用,提高代码复用性)和精细化管理。


3. list_iterator:由于链表相较于vector等比较特殊,它不能用原生指针来达到我们想要的效果。因此我们要对链表结点的指针(原生指针)进行封装(对这个进行特殊的处理),通过运算符重载等的方式来达到我们想要的效果。


三种结构彼此相依:list基于list_node来组成,同时list又要依靠list_iterator来向外表现,list_node通过上面两个结构来向外展示。
既然基本的结构我们已经搞清楚了,接下来我们就要来看在这些结构之下衍生出来的东西,也就是它们的成员和成员函数,我们先来看最基础的list_node吧。


Part2. list_node的实现

我们先来看代码:


cpp 复制代码
template<class T>//模板 泛型编程
class list_node
{
public:
	T data;//存数据
	list_node<T>* next;//后驱节点
	list_node<T>* prev;//前驱节点

	list_node(const T& data=T())//默认构造函数
		:data(data)
		,next(nullptr)
		,prev(nullptr)
	{ }
};

这就是我们list_node的实现,看起来还是非常easy的。关键问题有两个:

1.为什么所有成员要设置为公有,按照封装的逻辑不是应该要设置为私有吗?

2.为什么没有析构函数等的其他默认成员函数?


首先,第一个。我们在实现list和list_iterator要频繁调用这个类里面的成员,因此我们要设为公有。


其次,第二个。上述我们也讲过了,我们通过list对list_node进行统一管理,list_node的析构交由list来完成。(关键我们如果不这么搞,创建出来的list_node出作用域就会被销毁,会影响list整个的运作。)
这样我们就介绍完了list_node,接下来我们来看看list_iterator,这个特殊的迭代器。


Part3. list_iterator的实现

我们依旧先来看代码:


cpp 复制代码
		template<class Ref, class Ptr>//为const_iterator做准备 简化返回值 增加可读性
		class list_iterator
		{
		private:
			typedef list_node Node;//简化代码
			typedef list_iterator<Ref, Ptr> Self;//同上
			Node* _node;
		public:
			friend class list<T>;//友元声明 模板类作为内部类 外部无法访问私有
			list_iterator(Node* node)//带参构造
				:_node(node)
			{
			}
			Ref operator*()
			{
				return _node->data;
			}
			Ref operator*()const//const重载
			{
				return _node->data;
			}
			Self& operator++()
			{
				_node = _node->next;
				return *this;
			}
			bool operator!=(const Self& it)const
			{
				return _node != it._node;
			}
			bool operator==(const Self& it)const
			{
				return !(*this != it);
			}
			Self& operator--()
			{
				_node = _node->prev;
				return *this;
			}
			Self operator++(int)//后置++
			{
				Self tem(*this);
				_node = _node->next;
				return tem;
			}
			Self operator--(int)//后置--
			{
				Self tem(*this);
				_node = _node->prev;
				return tem;
			}
			Ptr operator->()//<1>
			{
				return &_node->data;
			}
			Ptr operator->()const
			{
				return &_node->data;
			}
		};

		typedef list_iterator<T&, T*> iterator;//<2>
		typedef list_iterator<const T&, const T*> const_iterator;

上文是我们对list_iterator的实现,我们关键要在意两个那就是上文中的<1>和<2>,我们来看看吧:


Part3.1. list_iterator中"->"的重载

<1>:这是->的重载,为啥我们返回的是指针,按道理我们不是应该返回数据类型,也就是T吗?

首先我们先来看对迭代器要访问数据的流程:我们先定义一个迭代器叫it,我们要取到数据要先得到被他封装的list的原生指针也就是list_node,我们通过他才可以得到数据(因为数据data是它的成员)。然后我们通过list_node中的data得到他的地址,得到地址就可以直接->来访问了。

其中最后的操作是原生的(即是系统自己就有的,不与自己定义的类什么有关),所以在实际对于->的重载中,编译器就隐去了最后一步,也就是通过data地址来得到数据的过程。我们只要完成前面的过程,也就是得到他的地址。这就是->重载。


Part3.2. const_iterator的适配

<2>:我们为什么要有两个typedef来搞(生成两种迭代器),我们不是可以通过泛型来自动判断类型来生成相应的迭代器吗?

我们来思考一个场景:此刻我们的list为int类型,此时我们要使用const_iterator可以吗?不行。为什么?因为现在的迭代器类型源于一开始list的类型,我们确定了list的类型,就也确定了iterator的类型,无法改变。因此我们需要另一个const修饰的迭代器来应对不时之需。


这样我们就介绍完了list_iterator。接下来,我们来总览一下list的实现吧,故事来到终章:


Part4. 基于list_node和list_iterator的list实现

先来看看代码吧:


cpp 复制代码
	template<class T>
	class list
	{
	private:
		class list_node//将list_node设置为内部私有类 封装的思想
		{
		public:
			T data;
			list_node* next;
			list_node* prev;

			list_node(const T& data = T())
				:data(data)
				, next(nullptr)
				, prev(nullptr)
			{
			}
		};

		typedef list_node Node;
		Node* _head;
		size_t _size;

	public:

		template<class Ref, class Ptr>
		class list_iterator//由于list_iterator要被外部所使用所以要设置为公有
		{
		private:
			typedef list_node Node;
			typedef list_iterator<Ref, Ptr> Self;//仅限内部使用的别名
			Node* _node;
		public:
			friend class list<T>;
			list_iterator(Node* node)
				:_node(node)
			{
			}
			Ref operator*()
			{
				return _node->data;
			}
			Ref operator*()const
			{
				return _node->data;
			}
			Self& operator++()
			{
				_node = _node->next;
				return *this;
			}
			bool operator!=(const Self& it)const
			{
				return _node != it._node;
			}
			bool operator==(const Self& it)const
			{
				return !(*this != it);
			}
			Self& operator--()
			{
				_node = _node->prev;
				return *this;
			}
			Self operator++(int)
			{
				Self tem(*this);
				_node = _node->next;
				return tem;
			}
			Self operator--(int)
			{
				Self tem(*this);
				_node = _node->prev;
				return tem;
			}
			Ptr operator->()
			{
				return &_node->data;
			}
			Ptr operator->()const
			{
				return &_node->data;
			}
		};

		typedef list_iterator<T&, T*> iterator;
		typedef list_iterator<const T&, const T*> const_iterator;//设置为Public 方便外部使用
		typedef list_node Node;//同上

		list()//list的函数 开始统一调用上面的list_node和list_iterator了
		{
			empty_init();//空构造 因为我们的这个链表一定要有哨兵位而且在一开始要让他的prev和next自指 把这些操作都封装为一个函数 简化代码
		}
		~list()
		{
			clear();//代码复用
			delete _head;
			_head = nullptr;
		}
		list(const list<T>& copy_l)//拷贝构造
		{
			empty_init();
			const_iterator it = copy_l.begin();
			while (it != copy_l.end())
			{
				push_back(*it);
				++it;
			}
		}
		list(std::initializer_list<T> il)
		{
			empty_init();
			for (auto& e : il)
			{
				push_back(e);
			}
		}
		list<T>& operator=(list<T> tem)//赋值重载现代写法
		{
			swap(tem);
			return *this;
		}
		void swap(list<T>& l)//自写swap 节省性能
		{
			std::swap(_head, l._head);
			std::swap(_size, l._size);
		}
		void empty_init()
		{
			_head = new Node;
			_head->next = _head;
			_head->prev = _head;
			_size = 0;
		}
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}
		/*void push_back(const T&x)
		{
			Node* temp = new Node;
			temp->data = x;
			Node* prevp = _head->prev;
			prevp->next = temp;
			temp->prev = prevp;
			_head->prev = temp;
			temp->next = _head;
			_size++;
		}*/
		iterator begin()
		{
			/*iterator it(_head->next);
			return it;*/
			return _head->next;
		}
		iterator end()
		{
			/*iterator it(_head);
			return it;*/
			return _head;
		}
		const_iterator begin()const
		{
			return _head->next;
		}
		const_iterator end()const
		{
			return _head;
		}
		iterator insert(iterator it, const T& x = T())//<1>
		{
			Node* prevp = it._node->prev;
			Node* now = it._node;
			Node* tem = new Node;
			tem->data = x;
			tem->prev = prevp;
			prevp->next = tem;
			tem->next = now;
			now->prev = tem;
			_size++;
			return tem;
		}
		void push_front(const T& x = T())
		{
			insert(begin(), x);
		}
		void push_back(const T& x = T())
		{
			insert(end(), x);
		}
		size_t size()const
		{
			return _size;
		}
		iterator erase(iterator pos)//<1> 迭代器失效
		{
			assert(_size > 0);
			assert(pos != end());
			Node* tem = pos._node;
			Node* prevp = tem->prev;
			Node* nextp = tem->next;
			prevp->next = nextp;
			nextp->prev = prevp;
			delete tem;
			tem = nullptr;
			--_size;
			return nextp;
		}
		void pop_front()
		{
			erase(begin());
		}
		void pop_back()
		{
			erase(--end());
		}
		bool empty()const
		{
			return _size == 0;
		}
		void resize(size_t n, const T& val = T())
		{
			if (_size < n)
			{
				for (size_t i = _size; i < n; i++)
				{
					push_back(val);
				}
			}
			else
			{
				iterator it = begin();
				for (size_t i = 0; i < n; i++)
				{
					++it;
				}
				while (it != end())
				{
					it = erase(it);
				}
			}
		}

		T& front()
		{
			return *begin();
		}

		const T& front()const
		{
			return *begin();
		}

		T& back()
		{
			return *(--end());
		}

		const T& back()const
		{
			return *(--end());
		}
	};

上面主要还是内部类来满足封装的思想,还有一个问题那就是迭代器失效,我们来分别研究一下吧。


Part4.1. 封装思想在list体现

封装思想的一个体现就是:我们要让使用者只看到怎么使用而不让他们染指底层。具体的,在list中的表现就是我们不能让list_node能在外面使用。而我们涉及到这个一共有两个途径:

1.通过list_node本身访问

2.通过list_iterator来间接访问


我们先来看一:我们可以在list中直接把 list_node设置为私有内部类,从而达到一个容器隔离的效果,使得外部不能访问到list_node。


其次二:二是比较麻烦解决的。首先由于list_iterator需要在外面能被访问。因此我们不能把它设置为私有,只能是公有。同时我们在下面的list实现还需要list的_node成员。但这个也简单,由于list_iterator是list的内部类,对于它的私有成员list也是可以直接访问的。但是由于list_iterator是模板类,对于模板类这个是行不通的,所以最后我们只能把list设为list_iterator的友元。这样,_node在list_iterator作为公有的情况下,以私有的身份同时做到了能在list内部使用和在外部不能使用,体现了封装。


Part4.2. 迭代器失效

我们来看看erase会导致的迭代器失效问题:



erase会导致迭代器失效,但同样是改变节点的insert却不会,读者可以自行思考一下。


那我们怎么解决这个问题呢?那就是---返回值,我们通过返回it的下一个节点的迭代器,这个举措同样方便了我们在clear()的删除。
这样我们就介绍完了list,最后来看看其他函数吧。


Part5. 其他函数

我们先来看看代码:


cpp 复制代码
namespace tool
{
	template <class Container>//使用模板 使得这个函数可以适用于所有容器
	void printf_container(const Container& con)
	{
		typename Container::const_iterator it = con.begin();//可以适用所有容器的原因是可以遍历的容器都会有迭代器这个结构 这是统一接口的思想
		while (it != con.end())
		{
			std::cout << *it << std::endl;
			++it;
		}
	}
}

Part6. 结语

上文我们实现了list这个标准库里面经典的一个类,我们也体会到了封装和统一接口的思想。运用好这两个思想我们将来的代码质量会更加的符合工业化的标准。


希望这篇博客会给你带来帮助~~~


最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!

最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~

相关推荐
码云骑士1 小时前
【2.Java基础】Java常量与变量-从基本类型到类型转换全面掌握
java·开发语言
爱和冰阔落1 小时前
Ollama 本地大模型部署实战:从安装到 RAG 知识库完整指南
开发语言·大模型·php·ollama
泡^泡1 小时前
Python数据类型与运算符
开发语言·windows·python
刃神太酷啦1 小时前
MySQL 库表操作 +数据类型+ 基础概念全梳理----《Hello MySQL!》(2)
java·c语言·数据库·c++·vscode·mysql·adb
不爱编程的小陈1 小时前
Go语言GMP调度模型深度解析:高并发背后的精妙设计
开发语言·后端·golang
李子琪。1 小时前
谷歌“三剑客”与云计算基石:GFS、MapReduce、Bigtable 全栈解析及私有云落地实践
开发语言·编辑器·perl
xufengzhu2 小时前
Python库PyMySQL的使用指南
开发语言·python·pip
z落落10 小时前
C# 泛型方法(原理、类型推断、多泛型参数)+泛型效率(普通类型 VS Object装箱 VS 泛型)
开发语言·c#
L_090710 小时前
【C++】异常
开发语言·c++