【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.mzph.cn/news/702798.shtml

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

相关文章

2024年2月16日优雅草蜻蜓API大数据服务中心v1.1.1大更新-UI全新大改版采用最新设计ui·增加心率计算器·退休储蓄计算·贷款还款计算器等数接口

2024年2月16日优雅草蜻蜓API大数据服务中心v1.1.1大更新-UI全新大改版采用最新设计ui增加心率计算器退休储蓄计算贷款还款计算器等数接口 更新日志 前言&#xff1a;本次更新中途跨越了很多个版本&#xff0c;其次本次ui大改版-同步实时发布教程《带9.7k预算的实战项目layuiph…

JavaWeb——006MYSQL(DDLDML)

这里写目录标题 数据库开发-MySQL首先来了解一下什么是数据库。1. MySQL概述1.1 安装1.1.1 版本1.1.2 安装1.1.3 连接1.1.4 企业使用方式(了解) 1.2 数据模型1.3 SQL简介1.3.1 SQL通用语法1.3.2 分类 2. 数据库设计-DDL2.1 项目开发流程2.2 数据库操作2.2.1 查询数据库2.2.2 创…

vscode 设置打开中断的默认工作目录/路径

vscode 设置打开终端的默认工作目录/路径** 文章目录 vscode 设置打开终端的默认工作目录/路径**打开vscode&#xff0c;打开设置UI 或是设置JSON文件&#xff0c;找到相关设置项方式1&#xff1a;通过打开settings.json的UI界面 设置:方式2&#xff1a;通过打开设置settings.j…

ES通用查询页面使用说明

前言:ES语法比较复杂,需要专门的学习,而且查询工具不太友好, 对公司运维人员使用有点困难,所以花了个时间做了一个页面,方便运维人员使用,如下。 也不难,有兴趣的朋友可以私聊发源码。 开发帮助-ES数据查询 搜索 输入要查看的文档索引,文档类型后点【查询】即可 搜…

qt-C++笔记之事件过滤器

qt-C笔记之事件过滤器 —— 杭州 2024-02-25 code review! 文章目录 qt-C笔记之事件过滤器一.使用事件过滤器和不使用事件过滤器对比1.1.使用事件过滤器1.2.不使用事件过滤器1.3.比较 二.Qt 中事件过滤器存在的意义三.为什么要重写QObject的eventFilter方法&#xff1f;使用QO…

groovy:XmlParser 读 Freeplane.mm文件,生成测试案例.csv文件

Freeplane 是一款基于 Java 的开源软件&#xff0c;继承 Freemind 的思维导图工具软件&#xff0c;它扩展了知识管理功能&#xff0c;在 Freemind 上增加了一些额外的功能&#xff0c;比如数学公式、节点属性面板等。 强大的节点功能&#xff0c;不仅仅节点的种类很多&#xff…

时序预测 | Matlab实现基于GRNN广义回归神经网络的光伏功率预测模型

文章目录 效果一览文章概述源码设计参考资料效果一览 文章概述 1.时序预测 | Matlab实现基于GRNN广义回归神经网络的光伏功率预测模型 2.单变量时间序列预测; 3.多指标评价,评价指标包括:R2、MAE、MBE等,代码质量极高; 4.excel数据,方便替换,运行环境2020及以上。 广义回…

python 运算符总结

什么是运算符 什么是运算符? 先看如下示例 549 例子中&#xff0c;4 和 5 被称为操作数&#xff0c; 称为运算符。 而Python 语言支持以下类型的运算符: 算术运算符比较&#xff08;关系&#xff09;运算符赋值运算符逻辑运算符位运算符成员运算符身份运算符运算符优先级 …

OPENSSL-PKCS7入门知识介绍

1 PKCS7数据结构说明 p7包括6种数据内容&#xff1a;数据(data),签名数据&#xff08;sign&#xff09;&#xff0c;数字信封数据&#xff08;enveloped&#xff09;&#xff0c;签名数字信封数据&#xff08;signed_and_enveloped&#xff09;&#xff0c;摘要数据&#xff08…

软件测试过程中如何有效的开展接口自动化测试

一.简介 接口自动化测试是指使用自动化测试工具和脚本对软件系统中的接口进行测试的过程。其目的是在软件开发过程中&#xff0c;通过对接口的自动化测试来提高测试效率和测试质量&#xff0c;减少人工测试的工作量和测试成本&#xff0c;并且能够快速发现和修复接口错误&…

自己测试CSDN质量分3

你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好 质量分测试网址

智慧校园|智慧校园管理小程序|基于微信小程序的智慧校园管理系统设计与实现(源码+数据库+文档)

智慧校园管理小程序目录 目录 基于微信小程序的智慧校园管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、微信小程序前台 2、管理员后台 &#xff08;1&#xff09;学生信息管理 &#xff08;2&#xff09; 作业信息管理 &#xff08;3&#xff09;公告…

蓝桥杯算法赛 第 6 场 小白入门赛 解题报告 | 珂学家 | 简单场 + 元宵节日快乐

前言 整体评价 因为适逢元宵节&#xff0c;所以这场以娱乐为主。 A. 元宵节快乐 题型: 签到 节日快乐&#xff0c;出题人也说出来自己的心愿, 祝大家AK快乐! import java.util.Scanner;public class Main {public static void main(String[] args) {System.out.println(&qu…

栈和堆什么意思,Rust所有权机制又是什么

栈和堆什么意思 栈&#xff1a;存储基本数据类型和引用数据类型的指针引用(地址)&#xff0c;基本数据类型占据固定大小的内存空间。 堆&#xff1a;存储引用数据类型的值&#xff0c;引用数据类型包括对象&#xff0c;数组和函数&#xff0c;在堆中&#xff0c;引用数据类型…

AI论文速读 | 【综述】(LLM4TS)大语言模型用于时间序列

题目&#xff1a;Large Language Models for Time Series: A Survey 作者&#xff1a;Xiyuan Zhang , Ranak Roy Chowdhury , Rajesh K. Gupta and Jingbo Shang 机构&#xff1a;加州大学圣地亚哥分校&#xff08;UCSD&#xff09; 网址&#xff1a;https://arxiv.org/abs/…

JAVA工程师面试专题-《Redis》篇

目录 一、基础 1、Redis 是什么 2、说一下你对redis的理解 3、Redis 为什么这么快&#xff1f; 4、项目中如何使用缓存&#xff1f; 5、为什么使用缓存&#xff1f; 6、Redis key 和value 可以存储最大值分别多是多少&#xff1f; 7、Redis和memcache有什么区别&#xf…

Folx Pro Mac中文p破解版如何使用?为您带来Folx Pro 详细使用教程!

​ Folx pro 5 中文版是mac上一款功能强大的老牌加速下载软件&#xff0c;新版本的Folx pro整体界面非常的简洁和漂亮&#xff0c;具有非常好用的分类管理功能&#xff0c;支持高速下载、定时下载、速度控制、iTunes集成等功能。Folx pro兼容主流的浏览器&#xff0c;不但可以下…

开源世界的学术问题

自由软件基金会是1983年成立的&#xff0c;到现在是41年。正好很有意思的是&#xff0c;在去年还有一篇文章&#xff08;CSDN 的翻译&#xff09;&#xff0c;专门在质疑说成立 40 年的自由软件基金会是不是已经快不行了&#xff0c;所以我们会用这个标题叫做兴衰发展历程来介绍…

Excel的中高级用法

单元格格式&#xff0c;根据数值的正负分配不同的颜色和↑ ↓ 根据数值正负分配颜色 2-7 [蓝色]#,##0;[红色]-#,##0 分配颜色的基础上&#xff0c;根据正负加↑和↓ 2↑-7↓ 其实就是在上面颜色的代码基础上加个 向上的符号↑&#xff0c;或向下的符号↓ [蓝色]#,##0↑;[红色…

uni-app vue3 setup nvue中webview层级覆盖问题

核心就是这两行&#xff0c;&#x1f923;发现设置后不能点击了&#xff0c;这个玩意可能只能弹窗打开的时候动态的修改 position: static, zindex: 0onLoad(options > {loadWebview()})function loadWebview() {let pageInfo uni.getSystemInfoSync();width.value pageI…