【C++进阶】STL容器--list底层剖析(迭代器封装)

目录

前言

list的结构与框架

list迭代器

list的插入和删除

insert

erase

list析构函数和拷贝构造

析构函数

 拷贝构造

赋值重载

 迭代器拷贝构造、析构函数实现问题

 const迭代器

 思考

总结


前言

        前边我们了解了list的一些使用及其注意事项,今天我们进一步深入学习一下list容器;本文的主要内容是list底层数据结构剖析以及list的模拟实现与封装;

在这里插入图片描述

list的结构与框架

list是一个带头双向循环链表,对list的模拟实现的重难点在于迭代器的实现,以及const迭代器的实现;

list实现整体需要封装三个部分:list_node(节点)、list_iterator(迭代器)、list(链表)

首先我们需要先定义一个节点类:

template<class T>
struct list_node
{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& x = T()):_data(x),_next(nullptr),_prev(nullptr){}
};

         定义时的基本结构和C语言的很像,节点中存储两个指针,一个指向下一个节点,一个指向前一个节点,以及T类型的值(便于存储各种类型的数据);

         其次就是 list_node 的构造函数,我们在list中new节点时需要调用 list_node 的构造函数(new只要对于内置类型:开空间+调用构造函数)

list的基本结构:

class list
{typedef list_node<T> Node;
public:void empty_init() // 初始化开一个头节点,同时也为了方便拷贝构造与构造函数复用{_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}list(){empty_init();}
private:Node* _head;size_t _size;//记录链表长度
};

 list结构初始化需要开一个头节点,并且指针都指向自己;

接下来我们实现一下push_back(尾插),这样我们就可以先上手测试list基本结构是否完善;

 具体操作如下,可以根据图先动手尝试写一下:

 具体代码如下:

void push_back(const T& x)
{Node* newnode = new Node(x);Node* tail = head->next;// 插入节点tail->next = newnode;newnode->prev = tail;newnode->_next = _head;_head->prev = newnode;_size++;
}

 能够插入数据后,接下来就是遍历list,最基本的也就是迭代器遍历,前边我们使用迭代器的方法:

list<int>::iterator it = lt.begin();
while (it != lt.end())
{cout << *it << ' ';it++;
}

vector是顺序结构,它有天然的迭代器,那list是链表怎么++进行遍历?list的 “ ++ ” 本质其实就是封装+运算符重载,我们需要重载“ ++ ”,每次调用“ ++ ”时迭代器都移动到下一个节点;

         从上述迭代器的使用方法中可以看出,要想实现最基本的遍历迭代器起码要重载三部分:

!=、*(解引用)、++;

list迭代器

         要想实现list的遍历,我们首先需要实现list的迭代器,为什么要实现迭代器?

        list迭代器的实现最能体现的就是封装思想,封装屏蔽底层的差异和实现细节;其目的是为了和其他容器的遍历修改的方式保持一致;

template <class T>
struct __list_iterator
{typedef list_node<T> Node;typedef __list_iterator<T,Ref,Ptr> self;Node* _node;//构造函数__list_iterator(Node* node):_node(node){}
};

 我们使用list的节点指针来初始化构造迭代器对象,以上便是迭代器的基本框架,接下来就是迭代器的基本功能实现;

// 前置
self& operator++()
{_node = _node->_next;return *this;
}self& operator--()
{_node = _node->_prev;return *this;
}//后置
self operator++(int)
{self tmp(*this);_node = _node->_next;return tmp;
}self operator--(int)
{self tmp(*this);_node = _node->_prev;return tmp;
}T& operator*()//解引用返回节点的数据即可
{return _node->_data;
}T* operator->()//通过指针访问成员,常用于自定义类型
{return &_node->_data;
}//迭代器比较,比较的是指针是否相等,如果指针相等,则它们指向同一节点
bool operator!=(const self& s)
{return s._node != _node;
}

这些功能实现以后,我们还不能直接使用迭代器,我们需要把迭代器包装到我们实现的list当中;

在list里边实现begin( )、end( );

class list
{public:typedef list_node<T> Node;typedef __list_iterator<T> iterator;void empty_init() {_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}list(){empty_init();}iterator begin();iterator end();
private:Node* _head;size_t _size;
};

 在实现begin( )、end( );之前,我们需要明确它们指向的位置是哪里?

iterator begin()
{return _head->_next;//返回时会自动调用iterator构造函数
}iterator end()
{return _head;
}

list的插入和删除

         我们在list的基础上包装了一次iterator,那么接下来我们就在有迭代器的基础上实现insert和erase操作;

insert

根据传进来的迭代器的位置进行插入操作:

 

iterator insert(iterator pos, const T& x)
{Node* newnode = new Node(x);Node* cur = pos._node;Node* prev = cur->_prev;// 插入节点newnode->_next = cur;cur->_prev = newnode;prev->_next = newnode;newnode->_prev = prev;_size++;return iterator(newnode);
}

 小tips

         双向循环链表在插入链接时很容易出现节点丢失的情况,为了尽可能的避免,这里推荐创建一个变量来记录当前节点的前一个位置;

erase

iterator erase(iterator pos)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;delete cur;prev->_next = next;next->_prev = prev;--_size;return iterator(next);
}

注意:

        erase之后迭代器会失效,因为迭代器指向的节点已经被删除释放,为了避免非法访问,这里我们应该返回下一个节点;insert根据客观性来讲,应该返回新插入节点;

 有了插入和删除,在尾插、尾删、头插、头删的接口中都可以复用插入删除操作:


void push_back(const T& x)
{insert(end(), x);
}void push_front(const T& x)
{insert(begin(), x);
}void pop_back(const T& x)
{erase(--end());
}void pop_front(const T& x)
{erase(begin());
}

list析构函数和拷贝构造

有了迭代器析构函数和拷贝构造就会非常简单,我们可以复用前边实现的功能

析构函数

void clear()
{iterator it = begin();while (it != end()){it = erase(it);//注意删除之后需要更新it(迭代器),erase自动返回下一个节点位置}
}//析构函数
~list()
{clear();delete _head;//注意释放头节点_head = nullptr;
}

 拷贝构造

list(list<T>& lt)
//这里应该是list(const list<T>& lt),但这样写需要const迭代器,const迭代器下面会进行实现
//所有这里先不用const修饰
{empty_init();for (auto e : lt){push_back(e);}
}

赋值重载

这里说传统写法,在复用前边功能实现也是非常简单

list<int>& operator=(list<int>& lt)
{if (this != &lt){clear();for (auto e : lt){push_back(e);}}return *this;
}

现代写法:

void swap(list<T>& lt)
{std::swap(_head, lt._head);std::swap(_size, lt._size);
}
list<int>& operator=(list<int> lt)
{swap(lt);return *this;
}

 迭代器拷贝构造、析构函数实现问题

普通迭代器基本已经实现完毕,那我们来思考一下,迭代器要不要实现析构函数和拷贝构造?

        切记,迭代器不需要实现析构函数,如果实现析构函数释放空间就会把list节点释放;把节点指针给迭代器只是为了上迭代器能够访问list;

        迭代器的拷贝构造和赋值重载也不需要实现,迭代器存在的目的只是为了模拟指针去访问,如果自己实现进行深拷贝那还怎么访问list节点;

 const迭代器

         在实现之前我们需要先捋清楚const迭代器const修饰的是什么?

        

 const迭代器修饰的是iterator指向的对象,对象不能修改;

在普通迭代器前边加上const修饰(const iterator),它修饰的是iterator迭代器,迭代器要能够修改,因为我们要使用 it++向后遍历,而我们需要的是内容不能被修改;

所以要想实现内容不能修改,我们需要重新实现一个类,不能单纯的使用const修饰普通迭代器;

 我们先使用简单的方法,再封装一个和iterator相似的类,命名为const_iterator;

再封装一个类它的内容和iterator的代码很类似,这样代码复用率很低,这里我们选择使用模板来实现一个类模板,可供普通迭代器和const迭代器使用;

 这里我们可以给iterator多添加两个模板参数:


template <class T, class Ref, class Ptr >
// Ref引用   Ptr指针
struct __list_iterator
{typedef list_node<T> Node;typedef __list_iterator<T,Ref,Ptr> self;Node* _node;
}

 为什么要添加?

在iterator类中支持修改的接口就俩个:

  • operator*()
  • operator->()

 区别就在于它们的返回值,普通迭代器返回的是T&、T*;

const_iterator返回的是const T&、const T*;

        它们是完全不同的类型,如何做到一个函数可以返回两种类型的参数,唯有模板参数,所以这里我们添加两个参数,一个用来返回指针类型(T* 和 const T*),一个用来返回引用(T& 和  constT&) ;

这样我们只需修改两个接口:

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

修改之后我们就可以把它包装在自己实现的list中:

class list
{public:typedef list_node<T> Node;typedef __list_iterator<T> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;//...iterator begin();iterator end();const_iterator begin() const;// 内容不需要修改只需对函数进行修饰,修改返回类型//因为返回的list节点指针可以构造iterator,也可以构造const_iteratorconst_iterator end() const;  // 成员函数后加const修饰的是成员函数,// 成员函数不可以修改对象的成员变量
private:Node* _head;size_t _size;
};

 我们写一个函数来测试一下:

void print_list(const list<int>& lt)
{list<int>::const_iterator it = lt.begin();while (it != lt.end()){//*it = 10;cout << *it << " ";++it;}cout << endl;for (auto e : lt){cout << e << " ";}cout << endl;
}

 list的模拟实现很好是体现了封装思想,也让我们很好的感受到泛型编程的魅力;

 思考

         这个测试接口只可以输出 list<int> 类型;那么问题来了,如何修改让它可以适用于任何类型?又如何让它能够遍历任何容器(vector、list ... 都可以使用)?

如何修改让它可以适用于list的任何类型?其实很简单,添加一个模板参数即可;

template<typename T>
void print_list(const list<T>& lt)
{// 使用class时,list<T>未实例化的类模板,编译器不能识别进行实例化// 前面加一个typename就是告诉编译器,这里是一个类型,等list<T>实例化后// 再去类里面去取并实例化出迭代器对象// 这也就是class和typename的区别typename list<T>::const_iterator it = lt.begin();while (it != lt.end()){//*it = 10;cout << *it << " ";++it;}cout << endl;for (auto e : lt){cout << e << " ";}cout << endl;
}

 如何让它能够遍历任何容器(vector、list ... 都可以使用),这里只需要再抽象一层,T可以是任何数据类型,vecror<string>、list<int>都是数据类型,由于每个容器的迭代器使用方式都基本一致,所以我们可以不指定遍历容器的迭代器,相同的使用方式就可以遍历其他的容器:

template<typename Con>
void print_container(const Con& con)
{typename T::const_iterator it = con.begin();while (it != con.end()){cout << *it << " ";++it;}cout << endl;
}

 完整代码:list模拟实现

内含反向迭代器的实现,目前阶段可以忽视


总结

        list模拟实现的目的就是为了更深刻的感受封装,以及泛型编程, list的模拟实现很好是体现了封装思想,也让我们很好的感受到泛型编程的魅力;以上便是本文的全部内容,希望对你有帮助,感谢阅读!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/265158.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode53题:最大子数组和(python3)

代码思路&#xff1a; 动态规划&#xff0c;使用动态规划如果上一个数是大于0&#xff0c;则加上&#xff1b;如果小于0直接用0。这样做的好处就是最终直接是最大子数组和。 class Solution:def maxSubArray(self, nums: List[int]) -> int:for i in range(1,len(nums)):nu…

ubuntu+QT+ OpenGL环境搭建和绘图

一&#xff0c;安装OpenGL库 安装OpenGL依赖项&#xff1a;运行sudo apt install libgl1-mesa-glx命令安装OpenGL所需的一些依赖项。 安装OpenGL头文件&#xff1a;运行sudo apt install libgl1-mesa-dev命令来安装OpenGL的头文件。 安装GLUT库&#xff1a;GLUT&#xff08;Ope…

express+mysql+vue,从零搭建一个商城管理系统5--用户注册

提示&#xff1a;学习express&#xff0c;搭建管理系统 文章目录 前言一、新建user表二、安装bcryptjs、MD5、body-parser三、修改config/db.js四、新建config/bcrypt.js五、新建models文件夹和models/user.js五、index.js引入使用body-parser六、修改routes/user.js七、启动项…

数字化运维与AIOps

干掉传统运维的不是devops&#xff0c;不是容器化&#xff0c;而是AI。随着未来基础设施的膨胀和复杂度急剧提升&#xff0c;人类运维能力已经显得力不从心。运维最终的归宿一定是人类决策&#xff0c;AI汇报与执行。 什么是数字化运维 数字化运维是一种基于信息技术手段数字化…

【GB28181】wvp-GB28181-pro部署安装教程(Ubuntu平台)

目录 前言1 安装依赖2 安装MySQL3 安装redis4 编译ZLMediaKit代码及依赖下载编译运行&#xff08;如果要运行wvp整个项目&#xff0c;这步可以先不执行&#xff09; 5 编译wvp-pro下载源码&#xff08;建议从github上下载&#xff0c;gitee上维护有时候不是很同步&#xff09;编…

USB Micro引脚及相应原理图绘制

前言&#xff1a;博主为实现绘制USB Micro输入口原理图&#xff0c;首先在 GD32F103XX的数据手册中找到引脚的功能描述&#xff0c;找到USBDM与USBDP功能&#xff0c;分别为引脚PA11与引脚PA12。然后进行相应的原理图绘制。 * USBDM。USBDM 引脚是与通用串行总线 (Universal Se…

Python 光速入门课程

首先说一下&#xff0c;为啥小编在即PHP和Golang之后&#xff0c;为啥又要整Python&#xff0c;那是因为小编最近又拿起了 " 阿里天池 " 的东西&#xff0c;所以小编又不得不捡起来大概五年前学习的Python&#xff0c;本篇文章主要讲的是最基础版本&#xff0c;所以比…

年龄性别预测4:C/C++实现年龄性别预测和识别(含源码,可实时预测)

年龄性别预测4&#xff1a;C/C实现年龄性别预测和识别(含源码&#xff0c;可实时预测) 目录 年龄性别预测4&#xff1a;C/C实现年龄性别预测和识别(含源码&#xff0c;可实时预测) 1.年龄性别预测和识别方法 2.人脸检测方法 3.年龄性别预测和识别模型(Python) &#xff0…

prometheus+grafana监控nginx的简单实现

1.编译安装NGINX 加入编译安装nginx-module-vts模块,目的是为了获取更多的监控数据(虚拟主机&#xff0c;upstream等) nginx下载 http://nginx.org/download/nginx-1.20.2.tar.gz nginx-module-vts下载 https://github.com/vozlt/nginx-module-vts/archive/refs/tags/v0.2…

【Docker】安装及相关的命令

目录 一 Docker简介 1.1 是什么 1.2 优缺点 1.3 应用场景 1.4 安装 二 命令 2.1 Docker基本命令 2.2 Docker镜像命令 2.3 Docker容器命令 一 Docker简介 1.1 是什么 Docker是一个开源的应用容器引擎&#xff0c;它基于Go语言实现&#xff0c;并利用操作系统本身已有的…

【亚马逊云】跨AWS账号创建复制规则同步S3存储桶中的数据

文章目录 注意事项一、创建存储桶【创建方&接收方完成操作】二、上传数据至bucket-transmit待同步测试三、创建复制规则【创建方完成操作】四、接收复制的对象【接收方完成操作】五、创建复制任务【创建方操作】六、运行批处理操作【创建方完成操作】七、检查是否完成跨账号…

leetcode:134.加油站

解题思路&#xff1a;需要注意开始时的编号&#xff0c;有的可以走一圈&#xff0c;有的走不了 模拟过程&#xff1a;for循环主要是用来模拟线性的过程&#xff0c;而在这里它是环状的&#xff1b; 可以用暴力解法&#xff0c;但是在这里我用贪心来解决。 常见疑惑&#xff1…

Django配置静态文件

Django配置静态文件 目录 Django配置静态文件静态文件配置调用方法 一般我们将html文件都放在默认templates目录下 静态文件放在static目录下 static目录大致分为 js文件夹css文件夹img文件夹plugins文件夹 在浏览器输入url能够看到对应的静态资源&#xff0c;如果看不到说明…

网络安全之内容安全

内容安全 攻击可能只是一个点&#xff0c;防御需要全方面进行 IAE引擎 DFI和DPI技术--- 深度检测技术 DPI --- 深度包检测技术--- 主要针对完整的数据包&#xff08;数据包分片&#xff0c;分段需要重组&#xff09;&#xff0c;之后对 数据包的内容进行识别。&#xff08;应用…

力扣5. 最长回文子串(双指针、动态规划)

Problem: 5. 最长回文子串 文章目录 题目描述思路复杂度Code 题目描述 思路 思路1&#xff1a;双指针 1.我们利用双指针从中间向两边扩散来判断是否为回文串&#xff0c;则关键是找到以s[i]为中心的回文串&#xff1b; 2.我们编写一个函数string palindrome(string &s, in…

大数据可视化的设计规范,全面剖析,很实用。

大数据可视化的设计规范需要考虑到数据量大、复杂度高、数据类型多样等特点。以下是一份常见的大数据可视化设计规范&#xff0c;供您参考&#xff1a; 设计原则 简单易用&#xff1a;保证用户操作简单、直观&#xff0c;降低用户认知负担。数据准确&#xff1a;保证数据准确…

数据结构-关键路径

介绍 在AOV网的基础上&#xff0c;如果用对应边来表示活动持续时间&#xff0c;这种有向图被称为AOE网在AOE网中&#xff0c;入度为0的为源点&#xff0c;出度为0的为汇点&#xff0c;整张网看做是一件事情完成的过程&#xff0c;那么这两个点就是事情的开始和结束。每个活动持…

阿里云ECS服务器vCPU是什么意思?

阿里云ECS服务器vCPU和CPU是什么意思&#xff1f;CPU和vCPU有什么区别&#xff1f;一台云服务器ECS实例的CPU选项由CPU物理核心数和每核线程数决定&#xff0c;CPU是中央处理器&#xff0c;一个CPU可以包含若干个物理核&#xff0c;通过超线程HT&#xff08;Hyper-Threading&am…

C#,弗洛伊德-瑞文斯特(Floyd-Rivest)算法与源代码

Robert W. Floyd 1 Floyd-Rivest 算法 Floyd-Rivest 算法是一种选择算法&#xff0c;用于在不同元素的数组中找到第k个最小元素。它类似于快速选择算法&#xff0c;但在实际运行中有更好的运行时间。 和 QuickSelect 一样&#xff0c;该算法基于分区的思想工作。对数组进行分…

洛谷C++简单题小练习day21—梦境数数小程序

day21--梦境数数--2.25 习题概述 题目背景 Bessie 处于半梦半醒的状态。过了一会儿&#xff0c;她意识到她在数数&#xff0c;不能入睡。 题目描述 Bessie 的大脑反应灵敏&#xff0c;仿佛真实地看到了她数过的一个又一个数。她开始注意每一个数码&#xff08;0…9&#x…