当前位置: 首页 > news >正文

【C++】list的模拟实现+迭代器的设计思维

目录

  • 1.认识STL中的list
  • 2.迭代器的设计思维
    • 2.1 迭代器的定义
    • 2.2 迭代器的底层结构
  • 3.list的模拟实现
    • 3.1 list的节点
    • 3.2 list的迭代器
    • 3.3 list类
  • 4.list和vector的比较


1.认识STL中的list

💨相比于vector简单的连续线性结构,list就稍显复杂了。
💨list是一个带头双向循环链表,其结构如图所示。

在这里插入图片描述

list的使用较为简单,不再赘述。这里直接放出list的使用文档,以供参考。
📝list使用文档

本文的重点在于通过list迭代器初步了解STL六大组件之一 —— 迭代器的设计理念


2.迭代器的设计思维

💭学习vector的时候,我们粗略地提过迭代器,当时我们说将其视作一个原生指针来使用(事实也是如此,因为vector迭代器的底层就是一个原生指针)。然而,迭代器不一定是一个指针,也可能是一个类(class type),为什么呢?想搞清楚迭代器的设计思维,要先搞懂迭代器的模式定义。

2.1 迭代器的定义

iterator的模式定义:(引用自《STL源码剖析》)
“提供一种方法,使之能够依序巡访某个聚合物(容器)所含的各个元素,而又无需暴露该聚合物的内部表述方式。”

💡通俗的理解,迭代器设计的本意就是为了给所有容器提供一种通用的访问方式,让使用者无需关注底层的数据结构。


2.2 迭代器的底层结构

💭了解了迭代器的定义,我们现在要着手于迭代器的实现。我们拿list的迭代器来举例子。
🌰 list不再能够像vector一样直接用原生指针作为迭代器,因为list的节点不保证在存储空间中连续存在。list迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员访问等操作。所谓“递增、递减、取值、成员访问”操作是指:

  • 递增:指向当前节点的下一节点;
  • 递减:指向当前节点的上一节点;
  • 取值:读取当前节点数据域中的值;
  • 成员访问:访问数据域中类对象的成员

🔎很显然,原生指针无法满足这些操作。所以,STL中迭代器的实现再次采用了类封装的思想来解决这一问题,将指针封装成一个迭代器类,并通过运算符重载来控制迭代器的操作,以达到每一种不同的容器都有其特有的迭代器。

假设list有一个迭代器it,那么++it就是让it指向当前指向节点的下一节点(递增),--it让it指向当前指向节点的上一节点(递减),*it是读取当前指向节点数据域中的值(取值),it->member是访问数据域中类对象的成员(这个有点特殊,后面模拟实现时详谈)

🔎况且,即使迭代器是一个类,但由于其只有一个指针类型的成员变量,其大小依然是4个字节(32位平台下),所以我们可以将迭代器视为一种大小与指针相同,行为与指针类似的对象。

迭代器 it 的结构大致如下:
在这里插入图片描述


3.list的模拟实现

💭了解了迭代器的底层构造,接下来我们就可以着手list的模拟实现了!

💦list的实现需要三个类:

  1. list的节点
  2. list的迭代器
  3. list本身

3.1 list的节点

🔎list的节点和list本身的结构是不一样的,因此要分开实现。list节点的结构图如下:
在这里插入图片描述

💬代码实现

// list的节点
template <class T>
struct _list_node
{
	typedef _list_node<T>* pointer;

	pointer prev; // 指向前一个节点
	pointer next; // 指向下一个节点
	T data;// 数据域

	_list_node(const T& val = T()) // 提供一个全缺省构造函数
		:data(val)
	{}
};

3.2 list的迭代器

基于2.2中的分析,我们来实现list的迭代器

⭕初步实现

// list的迭代器
template <class T>
class ListIterator
{
	typedef _list_node<T>* pNode;
	typedef ListIterator<T> Self;
	
public:
	pNode _pNode; //迭代器内当然要有一个指针,指向list的节点。这也是迭代器唯一的成员变量
	
public:
	ListIterator(pNode p = nullptr) // 用一个节点的指针构造迭代器
		:_pNode(p)
	{}
	
	T& operator*() // 取值
	{
		return _pNode->data;
	}

	T* operator->() // 成员访问
	{
		return &_pNode->data;
	}

    // 递增
	Self operator++() // 前置++
	{
		_pNode = _pNode->next;
		return *this;
	}

	Self operator++(int) // 后置++
	{
		Self tmp(_pNode);
		_pNode = _pNode->next;
		return tmp;
	}
    
    // 递减
	Self operator--() // 前置--
	{
		_pNode = _pNode->prev;
		return *this;
	}

	Self operator--(int) // 后置--
	{
		Self tmp(_pNode);
		_pNode = _pNode->prev;
		return tmp;
	}

	bool operator==(const Self& it)
	{
		return _pNode == it._pNode;
	}

	bool operator!=(const Self& it)
	{
		return _pNode != it._pNode;
	}
};

❓上面的代码乍一看似乎没什么问题,其实还不够完整。

我们知道,迭代器是一种行为类似指针的对象,而指针的各种行为中最常见也最重要的两种行为就是内容提领成员访问了,因此,迭代器中最重要的就是对operatoroperator->进行重载工作。而上面的代码问题就出在这两个最重要的地方。怎么回事呢?我们一步一步分析。

阅读文档我们知道,list不仅有iterator类型迭代器,还有const_iterator类型的迭代器该类型迭代器的特性是其指向对象受const保护,无法修改。

在这里插入图片描述

const_iterator是区别于iterator的另一种类型的迭代器,而不是简单在iterator前加上cosnt。

⭕区分

// 下面使用的是std中的list
list<int> lt(10, 1);

const list<int>::iterator it1 = lt.begin(); // 保护的是it1本身
//it1 = lt.end(); //错误,it1被const保护了,无法改变

list<int>::const_iterator it2 = lt.begin(); // 保护的是it2指向的对象
//*it2 = 2; //错误,it2指向的对象被const保护了,无法改变

💡由于const_iterator与iterator特性的不同,其实现方式也有所不同。上面我们实现的迭代器实际上是iterator类型的,根据const_iterator的特性,不难发现,只需在iterator的这两个地方做出调整即可变为const_iterator。

const T& operator*() // T& -> const T& // const保护*提取的返回值
{
	return _pNode->data;
}

const T* operator->() // T* -> const T* // const保护->访问的值
{
	return &_pNode->data;
}

那么,要重载一个迭代器类以实现const_iterator吗?不,这样的代码太过冗余,因为const_iterator与iterator的区别微乎其微。实现STL的大佬们可不会这么干。由于二者的区别只有在两个成员函数的返回值类型,因此,可以增加两个模板参数,根据传入的模板参数确定适应的返回值类型。

💬list迭代器最终代码

// list的迭代器类模板
template <class T, class Ref, class Ptr>
class ListIterator
{
	typedef _list_node<T>* pNode;
	typedef ListIterator<T, Ref, Ptr> Self;
	
public:
	pNode _pNode;

public:
	ListIterator(pNode p = nullptr)
		:_pNode(p)
	{}

	Ref operator*()
	{
		return _pNode->data;
	}

	Ptr operator->() //返回节点数据域的指针 
	{
		return &_pNode->data;
	}

	Self operator++() // 前置++
	{
		_pNode = _pNode->next;
		return *this;
	}

	Self operator++(int) // 后置++
	{
		Self tmp(_pNode);
		_pNode = _pNode->next;
		return tmp;
	}

	Self operator--() // 前置--
	{
		_pNode = _pNode->prev;
		return *this;
	}

	Self operator--(int) // 后置--
	{
		Self tmp(_pNode);
		_pNode = _pNode->prev;
		return tmp;
	}

	bool operator==(const Self& it)
	{
		return _pNode == it._pNode;
	}

	bool operator!=(const Self& it)
	{
		return _pNode != it._pNode;
	}
};

💡这样一来,只需在list类中加入这两个typedef,即可利用同一类模板实现const_iterator与iterator两个不同的类。

typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;

3.3 list类

💨实现了list的节点和list的迭代器后,list类就很容易实现了,无非就是各种结构的耦合和增删查改。根据list的结构特性,我们可以先实现insert和erase成员函数,其它成员函数只需复合使用即可,大大提高代码的复合性。废话不多说,直接上代码。

// 链表
template <class T>
class list
{
	typedef _list_node<T> Node;
	typedef Node* pNode;
	// 迭代器
	typedef ListIterator<T, T&, T*> iterator;
	typedef ListIterator<T, const T&, const T*> const_iterator;
	
private:
	// 创建一个空链表,即头结点next和prev都指向自己
	void empty_initialize()
	{
		_head = new Node;
		_head->next = _head;
		_head->prev = _head;
	}
	
	pNode _head; // 指向链表的头结点
	long _size;// 记录链表的长度,解决了size()成员函数O(N)时间复杂度的问题
	
public:
	// 迭代器
	// begin指向头节点的下一位置,end指向头节点
	iterator begin()
	{
		return _head->next;
	}

	const_iterator begin() const
	{
		return _head->next;
	}
	
	iterator end()
	{
		return _head;
	}

	const_iterator end() const
	{
		return _head;
	}

	// 构造函数


	// 1.无参构造函数
	list()
	{
		empty_initialize();
	}

	// 2.fill
	list(size_t n, const T& val = T())
	{
		empty_initialize();
		while (n--)
		{
			push_back(val);
		}
	}

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

	// 3.拷贝构造
	
	//传统写法
	//list(const list<T>& lt)
	//{
	//	empty_initialize();
	//	for (const auto& e : lt) // 范围for的底层是迭代器
	//	{
	//		push_back(e);
	//	}
	//}
	
	//现代写法
	list(const list<T>& lt)
	{
		empty_initialize(); // 防止交换后析构野指针
		list<T> tmp(lt.begin(), lt.end());
		swap(tmp);
	}

	// 4.range
	template <class InputIterator>
	list(InputIterator first, InputIterator last)
	{
		empty_initialize();

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

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

	// Destructor
	~list()
	{
	    //clear()是释放除头节点之外的所有节点,析构是释放所有节点,这里复合使用即可
		clear();
		delete _head;
		_head = nullptr;
	}

	size_t size() const
	{
		return _size;
	}

	bool empty() const
	{
		return _head->next == _head;
	}

	// Modifiers
	void clear()
	{
		while (!empty())
		{
			pop_back();
		}
	}
	
	void swap(list<T>& lt) // 交换两个list的_head指向和_size即可
	{
		std::swap(_head, lt._head);
		std::swap(_size, lt._size);
	}
	
	// 重点
	iterator insert(iterator pos, const T& val = T())
	{
		// 开新节点
		pNode newNode = new Node(val);

		// 链接
		pNode pPrev = pos._pNode->prev;
		pNode pNext = pos._pNode;

		pPrev->next = newNode;
		newNode->prev = pPrev;
		newNode->next = pNext;
		pNext->prev = newNode;


		++_size;
		return iterator(newNode);
	}

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

		pNode pPrev = pos._pNode->prev;
		pNode pNext = pos._pNode->next;

		delete pos._pNode;

		pPrev->next = pNext;
		pNext->prev = pPrev;

		--_size;
		return iterator(pNext);
	}

	void push_back(const T& val)
	{
		insert(end(), val);
	}

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

	void push_front(const T& val)
	{
		insert(begin(), val);
	}

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

4.list和vector的比较

🔎list和vector是我们最常用的两个容器,它们各有优缺点,相辅相成。由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:

在这里插入图片描述
补充一点,在空间利用率方面,vector的扩容机制往往会导致空间浪费,而list则不存在这个问题,因为它是按需开辟空间的,对空间的利用十分精准。

相关文章:

  • AI 也会写代码了,但我并不担心
  • Java8 遍历List 使用stream().parallel()并发安全
  • 计算机毕业设计Java企业固定资产管理系统的设计实现(源代码+数据库+系统+lw文档)
  • 实验十一 级数与方程符号求解(MATLAB)
  • [附源码]Python计算机毕业设计Django疫情防控平台
  • Java - 通过反射进行赋值以及函数调用
  • OpUtils的网络扫描
  • 电力系统潮流计算(牛顿-拉夫逊法、高斯-赛德尔法、快速解耦法)【6节点 9节点 14节点 26节点 30节点 57节点】(Matlab代码实现)
  • Kafka消息队列大数据实战教程-第四篇(Kafka客户端Producer API)
  • 【从零开始学习深度学习】12. 什么是模型的训练误差?基于三阶多项式的欠拟合与过拟合训练过程演示
  • 腾讯数字孪生和To B简介
  • 【机器学习】Rasa NLU以及Rasa Core概念和语法简介(超详细必看)
  • OkHttp3 详解
  • java计算机毕业设计ssm线上拍卖系统设计6luor(附源码、数据库)
  • idea远程debug
  • CRC 循环冗余检验【计网必考】
  • 《Nuitka打包实战指南》实战打包Playwright
  • 搜题接口系统
  • 极客时间Kafka - 05 Kafka 生产者发送消息可靠性保障|幂等生产者和事务生产者
  • Springboot 使用 Mybatis 启动失败排查定位
  • 【表格单元格可编辑】vue-elementul简单实现table表格点击单元格可编辑,点击单元格变成输入框修改数据
  • ES7-ES13 新特性
  • 【C++】打开C++的大门
  • 【深度学习】U-Net和FCN具体分析
  • Linux下 git 上传与删除 的基本指令
  • Swift 新 async/await 同步机制小技巧:消除“多余”的 await 关键字
  • Github如何使用详细介绍(保姆级教学)
  • 【ardunio+sx1268】与【esp32+sx1268】实现不同主控单片机lora通讯
  • Linux常用命令——pvscan命令
  • Openharmony的编译构建--进阶篇1
  • 旅游管理专业学什么 难就业吗
  • 2022海南高考体育专业考试时间 什么时候考试
  • 云南楚雄高考时间2021具体时间:6月7日
  • 高考什么特长可以加分 政策有哪些
  • 2022年贵州高考218分能报什么大学 218分能上哪些院校
  • 2022大专石油化工专业毕业后待遇 工资高吗
  • 中国科学院大学2021年各省录取分数线及专业分数线
  • 学英语用点读笔好不好 有效果吗
  • 2021河南职业技术学院学费多少 各专业收费标准
  • 2022广西外语口试成绩查询时间公布 什么时候查分