【C++】list的模拟实现+迭代器的设计思维
创始人
2024-04-18 16:49:15
0

目录

  • 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 
struct _list_node
{typedef _list_node* pointer;pointer prev; // 指向前一个节点pointer next; // 指向下一个节点T data;// 数据域_list_node(const T& val = T()) // 提供一个全缺省构造函数:data(val){}
};

3.2 list的迭代器

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

⭕初步实现

// list的迭代器
template 
class ListIterator
{typedef _list_node* pNode;typedef ListIterator 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 lt(10, 1);const list::iterator it1 = lt.begin(); // 保护的是it1本身
//it1 = lt.end(); //错误,it1被const保护了,无法改变list::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 ListIterator
{typedef _list_node* pNode;typedef ListIterator 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 iterator;
typedef ListIterator const_iterator;

3.3 list类

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

// 链表
template 
class list
{typedef _list_node Node;typedef Node* pNode;// 迭代器typedef ListIterator iterator;typedef ListIterator 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.filllist(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& lt)//{//	empty_initialize();//	for (const auto& e : lt) // 范围for的底层是迭代器//	{//		push_back(e);//	}//}//现代写法list(const list& lt){empty_initialize(); // 防止交换后析构野指针list tmp(lt.begin(), lt.end());swap(tmp);}// 4.rangetemplate list(InputIterator first, InputIterator last){empty_initialize();while (first != last){push_back(*(first++));}}//"="list operator=(list 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;}// Modifiersvoid clear(){while (!empty()){pop_back();}}void swap(list& 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则不存在这个问题,因为它是按需开辟空间的,对空间的利用十分精准。

相关内容

热门资讯

银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...