C++ list 模拟实现

 

目录

1. 基本结构的实现

2.  list()

3. void push_back(const T& val)

4. 非 const 迭代器

4.1 基本结构 

4.2 构造函数 

4.3 T& operator*()

4.4  __list_iterator& operator++()

4.5 bool operator!=(const __list_iterator& it)

4.6 T* operator->()

5. const 迭代器 

6. begin() && end()

​编辑

7. iterator insert(iterator pos, const T& val)

8. iterator erase(iterator pos)

9. void push_front(const T& val)

10. void pop_front()

11. void pop_back()

12. size_t size() const

13. void clear()

14. ~list()


1. 基本结构的实现

在 list 的使用部分,我们已经知道了 list 其实就是一个带头的双向循环链表 (以下简称双链表)。结合我们在 C 语言写过的双链表的实现:C语言数据结构初阶(4)----带头双向循环链表_姬如祎的博客-CSDN博客

第一步要做的就是定义出链表的节点。和 C 语言不一样的是,list 可以存储任意类型的数据,因此必须定义成模板。 老规矩为了与库中的 list 做区分,我们还是会把自己模拟实现的 list 放到一个命名空间里面。

namespace Tchey
{template<class T>struct ListNode{T _val;ListNode<T>* _prev;ListNode<T>* _next;ListNode(const T& val = T()):_val(val),_prev(nullptr),_next(nullptr){}};
}

在 C++ 中结构体被提升成了类,因此在定义双链表的节点的时候,我们可以写一个节点的构造函数,这样在堆上申请节点的时候,就能够直接初始化节点的 val 值啦!

OK,既然双链表的节点定义好了,list 类中的成员变量应该如何定义呢?起始很简单,list 类想要维护一个双链表只需要维护头结点的指针就可以了。因为 list 是带头的双链表,因此,list 的成员变量就是那个哨兵位的头结点的指针。

namespace Tchey
{template<class T>class list{private:ListNode<T>* _head;};
}

2.  list()

这个是 list 的无参构造函数,他的任务就是做好初始化哨兵位的头结点的工作。你还记得双向带头循环链表的初始化应该怎么做吗?看下面的图你应该就会记得了吧!

因为哨兵位的头结点是不需要存储任何有效的数据的,他的 val 值填多少都无所谓!索性就不填了,用默认的 val 就行。

list()
{_head = new ListNode<T>;_head->_next = _head;_head->_prev = _head;
}

3. void push_back(const T& val)

这个是双链表的尾插,双链表的尾节点,就是哨兵位的头结点的 _prev。开辟新节点,找到尾节点,做好链接工作就行啦!不知道如何链接,请看 C 语言实现的双向链表中的 push_back 函数:

C语言数据结构初阶(4)----带头双向循环链表_姬如祎的博客-CSDN博客

void push_back(const T& val)
{ListNode<T>* newNode = new ListNode<T>(val);ListNode<T>* tail = _head->_prev;tail->_next = newNode;newNode->_prev = tail;_head->_prev = newNode;newNode->_next = _head;
}

4. 非 const 迭代器

list 的迭代器是我们遇到的第一个不是原生指针的迭代器,我们来看看他的迭代器和 vector 迭代器的差别:

对于 vector 来说,他的迭代器是 int 类型的指针 (假设vector存储 int 数据),解引用就直接能访问到真实的数据。如果 list 也是原生指针,那只能是结构体的指针,解引用得到的就是一个结构体,无法拿到真实的数据。list 拿到数据需要通过访问结构体的 _val 成员得到。

对于 vector 来说,他的迭代器是 int 类型的指针 (假设vector存储 int 数据),并且 vector 的存储空间是连续的。迭代器加加就能直接跳过一个整形的空间,直接指向下一个有效的数据。

list 呢,物理空间不连续,如果 list 迭代器是结构体指针,加加之后访问到的空间并一定不属于你,可能会发生内存错误。实际上 list 的迭代加加之后,应该是指向节点的下一个节点,即需要通过 _next 找到下一个节点。

综上所述,list 的迭代器不可能是原生指针,需要对节点的指针进行封装。封装时,我们需要重载 *,++,等运算符,这样才能正确实现迭代器的效果。

4.1 基本结构 

我们已经知道了,list 的迭代器就是对原生的结构体指针进行封装。然后通过运算符重载实现迭代器应有的功能。那这个迭代器的成员变量当然就是一个节点的指针啦!

namespace Tchey
{template<class T>struct __list_iterator{public:private:ListNode<T>* _node;};
}

4.2 构造函数 

在 list 类里面,有 begin(),end() 之类的函数,用来返回一个迭代器对象,那么我们实现的迭代器就需要提供构造函数,支持使用节点的指针构造出来一个迭代器对象。

__list_iterator(ListNode<T>* node):_node(node)
{}

4.3 T& operator*()

我们都使用过迭代器遍历一个容器,知道了迭代器的解引用解释返回真实有效的数据,因此 operator 就是返回节点的 _val 值。

T& operator*()
{return _node->_val;
}

4.4  __list_iterator<T>& operator++()

对于 list 来说迭代器加加实际上就是让迭代器的成员变量 _node 指向当前节点的下一个节点。

__list_iterator<T>& operator++()
{_node = _node->_next;return *this;
}

4.5 bool operator!=(const __list_iterator<T>& it)

这个函数就是判断两个迭代器是否是一样的嘛,判断的逻辑就是判断两个迭代成员的节点指针是否相同。不能不写 const 因为 list 中的 begin(),end() 返回的都是一个迭代器的拷贝,当我们自己定义的迭代器变量与 end() 的返回值做 != 判断时就会调用 operator!= 如果没有 const,因为 end 的返回值具有常性,是无法用非 const 的迭代器类型来接收的。

bool operator!=(__list_iterator<T>& it)
{return _node != it._node;
}

4.6 T* operator->()

为什么要重载这种解引用的方式呢?emm,如果 list 容器存储的是一个结构体类型,或者其他自定义类型,那么重载了 -> 就能较为方便拿到我们的数据!

如果我没有重载 -> 那么当我们用迭代器区访问一个存储自定义类型的 list 的时候,你就需要这么写:

struct info
{int a;int b;
};int main()
{info in = { 10,20 };list<info> lt;lt.push_back(in);auto it = lt.begin();while (it != lt.end()){cout << (*it).a << " " << (*it).b << endl;it++;}return 0;
}

但是如果你重载了 -> 这个运算符,你就可以这么写: 

struct info
{int a;int b;
};int main()
{info in = { 10,20 };list<info> lt;lt.push_back(in);auto it = lt.begin();while (it != lt.end()){cout << it->a << " " << it->b << endl;it++;}return 0;
}

 我们下来看看怎么重载 -> 这个吧,其实重载 -> 就是返回 list 中的成员变量的 _val 的地址:

T* operator->()
{return &(_node->_val);
}

上面的例子中在使用迭代器遍历 list 的时候,it->a,本质上应该是先调用 operator-> 得到结构体的指针,再通过 -> 拿到数据,因此实际上应该这么写的:it->->a。但是这样写着实奇怪, 因此编译器会进行处理,我们只需要这么写就可以:it->a。 

5. const 迭代器 

首先,我们要理解 const 迭代器和非 const 迭代器的区别:

在 const 迭代器中解引用返回的数据是不能够更改的。

在 const 迭代器中 operator-> 返回的指针解引用得到的数据也是不能修改的!

也就是说在 const 迭代器中 operator* 的返回值应该是 const T&,operator-> 的返回值应该是 const T* 。然后,你一拍脑袋说,这好办哇!我们把非 const 迭代器的代码 copy 一份,改成 const 迭代器不就行了嘛!是的这样做完全没问题,但是,既然我们学了模板,这样的脏活累活自然得交给我们的编译器去做啦!

我们来看看怎么把 const 迭代器和非 const 迭代器融合成一个模板吧!const 迭代器和非 const 迭代器的不同就是右两个函数的返回值类型不同嘛,因此我们可以将函数的返回值的类型参数化,通过外部实例化的传递来决定是 const 迭代器还是非 const 迭代器!

因为有两个函数的返回值不相同,所以我们需要为我们封装的迭代器增加两个模板参数!

当我们传入 T&,T* 就是非 const 的 iterator,当我们传入 const T&,const T* 就是 const 的 iterator。是不是美妙绝伦!

__list_iterator 的模板参数改了,其他的函数也相应地改改嘛!因为给 __list_iterator 加上模板参数之后呢,这个类型写起来就比较复杂,我们可以使用 typedef 给他重新去一个名字

template<class T, class Ref, class Ptr>
struct __list_iterator
{
public:typedef __list_iterator<T, Ref, Ptr> self;__list_iterator(ListNode<T>* node):_node(node)
{}self& operator++()
{_node = _node->_next;return *this;
}bool operator!=(const self& it)
{return _node != it._node;
}Ref operator*()
{return _node->_val;
}Ptr operator->()
{return &(_node->_val);
}private:
ListNode<T>* _node;
};

6. begin() && end()

上面的那张图已经解释了如何定义 const 迭代器和非 const 迭代器啦!那么 begin() 与 end() 的 const 版本和 非 const 版本就是信手拈来了!

begin 对应的迭代器起始就是用 _head->_next 构造一个迭代器返回!

typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;iterator begin()
{return _head->_next;
}iterator end()
{return _head;
}const_iterator begin() const
{return _head->_next;
}const_iterator end() const
{return _head;
}

为啥可以直接返回节点的指针呢 ?因为单参数的构造函数支持隐式类型转化嘛!

其实 list 的模拟实现,难的就是迭代器的部分,好的,我们的迭代器就已经实现完毕了!懂的都懂嘛! 

7. iterator insert(iterator pos, const T& val)

我们先通过 pos 拿到节点的指针,申请节点,链接就行了,相当简单呢!最后返回新插入的节点的迭代器。

void insert(iterator pos, const T& val)
{ListNode<T>* cur = pos._node;ListNode<T>* prev = cur->_prev;ListNode<T>* newNode = new ListNode<T>(val);prev->_next = newNode;newNode->_prev = prev;newNode->_next = cur;cur->_prev = newNode;return newNode;
}

8. iterator erase(iterator pos)

 在 list 的使用哪一节,我们观察到了,erase 是不能删除 end() 位置的节点的,即不能删除哨兵位的头结点。我们可以使用断言检查。为了解决迭代器失效的问题呢,我们可以给 erase 增加一个返回值。

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

9. void push_front(const T& val)

在我们完成了 insert 接口和 erase 接口的实现之后,下面的插入删除可以完全复用啦!

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

10. void pop_front()

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

11. void pop_back()

这里的 -- 需要在迭代器里面重载,原理和 ++ 差不多,就交给你来实现啦!

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

12. size_t size() const

遍历一遍出结果,或者呢,你也可以在 list 里面维护一个表示 list 长度的变量。

size_t size() const
{int sz = 0;auto it = begin();while (it != end()){sz++;++it;}return sz;
}

13. void clear()

这个函数是清空 list 存储有效数据的节点,也就是说 clear 之后呢,哨兵位的头结点还在!

切记不可以 ++it,因为迭代器失效的问题嘛!我们通过 erase 的返回值来清除节点就行啦。

void clear()
{auto it = begin();while (it != end()){it = erase(it);}
}

14. ~list()

主打的就是一个复用。我们写完 clear 之后析构函数直接复用,然后再释放掉 _head 就可以啦!

~list()
{clear();delete _head;_head = nullptr;
}

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

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

相关文章

Vmware下的虚拟机NAT连接后仍然木有网络

问题描述 出现在主机能ping通&#xff0c;互联网ping不通的情况。 废话 假设已经设置了网络配置文件IPADDR。 那么&#xff0c;NAT后可以访问互联网的前提是&#xff1a;这个IPADDR的网段在Vmware软件设置的网段内。 解决 在Vmware虚拟网络设置选项卡中&#xff0c;进NAT配…

Openssl数据安全传输平台010:jasoncpp 0.10.7的编译 - Windows-vs2022 / Ubuntu/ Centos8 -含测试代码

文章目录 0. 代码仓库1 安装1.1 windows 下的安装1.2 Linux 下的安装1.2.1 相关环境配置问题1.2.2 准备安装1.2.2.1 安装scons1.2.2.2 安装jsoncppUbuntu系统下Centos8系统下 2 编译 c 测试文件&#xff1a; json-test.cpp2.1 配置库文件2.2 配置VS2.3 Winsows系统下cpp文件测试…

Unity ScrollView最底展示

Unity ScrollView最底展示 问题方案逻辑 问题 比如在做聊天界面的时候我们肯定会使用到ScrollView来进行展示我们的聊天内容&#xff0c;那么这个时候来新消息的时候就需要最底展示&#xff0c;我认为这里有两种方案&#xff1b; 一种是通过算法每一条预制体的高度*一共多少…

嵌入式-数码管控制

一、数码管显示数字&#xff0c;P2_4, P2_3,P2_2,这三个组合起来代表1-8的二进制表示代表1-8个led。P0代表要显示的数字的16进制表示。 这个图就是表示led灯, 比如要显示数据6&#xff0c;那就是0111 1101&#xff0c;那么16进制就是0x7D,所以p00x7D 二、数码管&#xff0c;动…

TYWZOJ 礼品配对包装 题解

文章目录 题目描述输入格式输出格式样例样例输入样例输出 数据范围与提示思路与部分实现完整代码 题目描述 爱与愁大神在这家目标店买了 2 x 2x 2x 份礼物&#xff0c;打算分给班级同学。其中有 x x x 份黑礼品&#xff0c; x x x 份白礼品&#xff0c; 2 x 2 2x2 2x2 个空…

Builder 请进:波卡最新开发入门指南

撰文&#xff1a;Dennis Zoma 编译&#xff1a;OneBlock 社区 本文更新于 2023 年 10 月 3 日&#xff0c;来源&#xff1a;https://wiki.polkadot.network/docs/build-guide Polkadot 是一个区块链协议&#xff0c;有两个目标&#xff1a;在所有连接的平行链之间提供共享安全…

如何能够在发现问题和提问的时候一并带出自己的解决方案

1. 充分理解问题&#xff1a; 在提出问题之前&#xff0c;确保你已经完全理解了问题的本质。从不同的角度分析问题&#xff0c;确保没有遗漏任何重要的信息或者上下文。 2. 进行自我调查和研究&#xff1a; 在向他人寻求帮助之前&#xff0c;尝试自己解决问题。利用网络资源…

vue项目package.json与package-lock.json作用及区别

package.json文件介绍和使用 运行项目&#xff0c;命令行: npm run dev “dependencies” 运行依赖&#xff0c;需引入页面使用 “devDependencies” 开发依赖(生产环境使用)&#xff0c;只是开发阶段需要 我们每次新建一个项目的时候会发现在项目中会有这么俩个相似的文件&am…

vue3基础流程

目录 1. 安装和创建项目 2. 项目结构 3. 主要文件解析 3.1 main.js 3.2 App.vue 4. 组件和Props 5. 事件处理 6. 生命周期钩子 7. Vue 3的Composition API 8. 总结和结论 响应式系统&#xff1a; 组件化&#xff1a; 易于学习&#xff1a; 灵活性&#xff1a; 社…

grafana InfluxDB returned error: error reading influxDB 400错误解决

问题&#xff1a; 如图提示错误解决 确认自己的docker容器是否配置了以下3个字段 DOCKER_INFLUXDB_INIT_USERNAMExxx DOCKER_INFLUXDB_INIT_PASSWORDyyy DOCKER_INFLUXDB_INIT_ADMIN_TOKENzzz 如果有&#xff0c;在grafana中需要添加header配置Header: Authorization , Value…

SSM宾馆客房管理系统开发mysql数据库web结构java编程计算机网页源码eclipse项目

一、源码特点 SSM 宾馆客房管理系统是一套完善的信息系统&#xff0c;结合springboot框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代 码和数据库&#xff0c;系统…

mac安装jenkins

1、安装jenkins之前确认是否安装了homebrew 2、开始安装jenkins 安装完如下图则安装完成 3、不想用默认ip和端口的自己改一下ip和端口 4、启动jenkins brew services restart jenkins 使用自己修改后的ip:port http://0.0.0.0:8088 根据提示信息&#xff0c;拿到管理员密码&…

Echarts 实现 设备运行状态图(甘特图) 工业大数据展示

let option{tooltip: {formatter: function (params) {let startTime new Date(params.value[1])let endTime new Date(params.value[2]);//北京时间/时间戳转成日常时间function convert(date){var y date.getFullYear();var m date.getMonth() 1;m m < 10 ? "0…

如何在 Azure 容器应用程序上部署具有 Elastic Observability 的 Hello World Web 应用程序

作者&#xff1a;Jonathan Simon Elastic Observability 是提供对正在运行的 Web 应用程序的可见性的最佳工具。 Microsoft Azure 容器应用程序是一个完全托管的环境&#xff0c;使你能够在无服务器平台上运行容器化应用程序&#xff0c;以便你的应用程序可以扩展和缩减。 这使…

Redis(01)| 数据结构

这里写自定义目录标题 Redis 速度快的原因除了它是内存数据库&#xff0c;使得所有的操作都在内存上进行之外&#xff0c;还有一个重要因素&#xff0c;它实现的数据结构&#xff0c;使得我们对数据进行增删查改操作时&#xff0c;Redis 能高效的处理。 因此&#xff0c;这次我…

Apriori介绍及代码批注

一、Apriori原理解析 1. 概述 关联规则分析是数据挖掘中最活跃的研究方法之一&#xff0c;目的是在一个数据集中找到各项之间的关联关系&#xff0c;而这种关系并没有在数据中直接体现出来。以超市的销售数据为例&#xff0c;当存在很多商品时&#xff0c;可能的商品组合数量…

【Opencv4快速入门】轮廓检测findContours

7.2 轮廓检测findContours 7.2.1 轮廓查找findContours7.2.2 轮廓绘制drawContours图像轮廓是指图像中对象的边界,是图像目标的外部特征,这个特征对于图像分析、目标识别和理解更深层次的含义具有重要的作用。 7.2.1 轮廓查找findContours 图像的轮廓补单能够提供物体的边缘,…

Java练习题2020-2

"统计1到N的整数中,除了1和自身之外&#xff0c;至少还能被两个数整除的数的个数 输入说明&#xff1a;整数 N(N<10000)&#xff1b; 输出说明&#xff1a;符合条件的数的个数 输入样例&#xff1a;10 输出样例&#xff1a;3 (说明&#xff1a;样例中符合条件的3个数是…

安卓开发实例:方向传感器

调用手机的方向传感器&#xff0c;X轴&#xff0c;Y轴&#xff0c;Z轴的数值 activity_sensor.xml <?xml version"1.0" encoding"utf-8"?> <androidx.constraintlayout.widget.ConstraintLayoutxmlns:android"http://schemas.android.c…

檢測項目簡體字

某些項目可能要求代碼中不允許使用簡體字 安裝stcheck檢查 yarn add stcheck --dev在項目根目錄創建 st.config.json 文件 {"patterns": ["./**/*.(ts|js|tsx|jsx|vue|html)","!**/node_modules/**","!.git/**"],"gitignore&q…