【c++丨STL】list模拟实现(附源码)

🌟🌟作者主页:ephemerals__
🌟🌟所属专栏:C++、STL

目录

前言

一、节点、迭代器以及函数声明

二、list功能实现

1. 节点

2. 迭代器

迭代器的默认构造

operator*

operator->

前置++和--

后置++和--

operator!=

3. 容器

交换两个容器的内容

迭代器接口

空链表初始化

构造函数

拷贝构造函数

赋值重载

析构函数

容量接口size

插入和删除

insert和erase

push_back、push_front、pop_back、pop_front

清空链表

三、程序全部代码

总结


前言

        通过之前对list的学习,我们已经基本掌握了其底层结构以及常用接口。今天我们在此基础上,尝试模拟实现list。

        与vector、string不同,由于list的底层是一个双向带头循环链表,所以它的实现上要更加复杂。vector和string的迭代器可以是原生指针,但是list的迭代器需要用一个类模板来实现,并且数据之间都是以节点的指针相连接,我们还需要定义其节点结构。当然,与vector一样,在接下来模拟实现list的过程中,我们也将采用类模板进行定义,并不会将声明和定义分离。

建议大家在掌握了双向带头循环链表以及vector、string的实现之后,再来阅读本文,否则其中部分内容可能较难理解。

一、节点、迭代器以及函数声明

        首先是链表节点、迭代器和我们要实现接口的声明:

#include <iostream>
#include <cassert>
using namespace std;//节点
template<class T>
struct list_node
{T _data;//数据域list_node<T>* _next;//指向后一个节点list_node<T>* _prev;//指向前一个节点//节点的默认构造list_node(const T& x = T());
};//迭代器
template<class T, class Ref, class Ptr>
struct list_iterator
{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;Node* ptr;//封装节点的指针//迭代器构造list_iterator(Node* node);//解引用Ref operator*();//结构成员间接访问Ref operator->();//前置++&Self operator++();//前置--&Self operator--();//后置++Self operator++(int);//后置--Self operator--(int);//不等于bool operator!=(const Self& it);
};//容器
template<class T>
class List
{typedef list_node<T> Node;
public:typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;//迭代器接口iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;//空链表的初始化void empty_init();//无参构造List();//n个val值构造List(size_t n, const T& val = T());//拷贝构造List(const List<T>& l);//赋值重载List<T>& operator=(List<T> l);//析构~List();//插入和删除iterator insert(iterator pos, const T& x);iterator erase(iterator pos);void push_back(const T& x);void push_front(const T& x);void pop_back();void pop_front();//清空链表void clear();//交换两个链表void swap(List<T> l);
private:Node* _head;//头节点的指针size_t _size;//节点个数
};//交换两个链表--非成员函数版
template <class T>
void swap(List<T>& l1, List<T>& l2);

接下来,我们开始逐一实现这三个部分。

二、list功能实现

1. 节点

        和传统的双向带头循环链表相同,节点当中有一个数据域用于存放数据;两个指针分别指向前驱节点后继节点。 我们需要实现节点的默认构造函数,便于容器构造节点。

代码实现:

//节点
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){}
};

可以看到,这里的节点我们也定义成了一个类模板,参数T表示的是传入的数据类型。对于构造函数,如果我们没有显式传参,则数据x就会调用其默认构造函数初始化。

2. 迭代器

        迭代器也是一个类模板,我们模拟实现的迭代器具有三个模板参数数据类型TT的引用类型Ref(普通引用或const引用)、T的指针类型Ptr(普通指针或const指针)。 为什么要这样设计呢?其实这样做的目的是为了提高代码复用率。如果我们创建了一个list普通对象和一个const对象,那么使用它们的迭代器时,相对应就会有普通迭代器const迭代器。这样我们在实现迭代器时,就要将同样的接口写两遍。而当我们定义了三个模板参数后,容器内部就可以通过传参来定义对应的普通迭代器ef和const迭代器,减少了代码冗余。

//list容器内部定义迭代器
typedef list_iterator<T, T&, T*> iterator;//普通迭代器
typedef list_iterator<T, const T&, const T*> const_iterator;//const迭代器

其次,为了简化代码,我们typedef一下节点类型以及迭代器本身类型:

typedef list_node<T> Node;//节点
typedef list_iterator<T, Ref, Ptr> Self;//迭代器本身

除此之外,由于迭代器是将指针进行封装,所以我们要将其成员变量设置为一个指向链表节点的指针

Node* _ptr;//封装节点的指针

接下来,我们开始实现迭代器的操作函数。

迭代器的默认构造

//迭代器构造
list_iterator(Node* node):_ptr(node)
{}

对于构造函数,我们将传入的指针赋值给成员指针即可。

operator*

//解引用
Ref operator*()
{return _ptr->_data;
}

解引用用于访问指针所指对象的数据,所以函数返回迭代器指向节点的数据域。

operator->

//结构成员间接访问
Ref operator->()
{return &_ptr->_data;
}

        正常来讲,该函数的作用应该是用于访问指针所指向数据的成员,但是它并没有参数,而且返回值却是数据地址。这是为什么呢?这其实和c++的语法规定有关。当我们使用该运算符重载时,要访问到数据的成员,原本应该这样写:迭代器 ->-> 成员。但是连续两个“->”看起来并不美观,并且难以理解,所以c++语法在这里省略了一个“->”。所以我们在实现该重载时,要返回数据的地址,编译器就会默认根据这个地址找到指定成员(成员名写在->后即可,不传参)

前置++和--

         由于链表底层结构并非连续,所以不能直接使指针++/--来改变指向,而应该使用类似链表遍历的方式。实现如下:

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

后置++和--

        后置++/--的实现方法与前置++/--类似。这里创建一个临时迭代器来保存未移动时的指向。

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

operator!=

//不等于
bool operator!=(const Self& it)
{return _ptr != it._ptr;
}

这里判断它们的成员指针是否相等即可。

3. 容器

        为了简化代码,还是先typedef一下节点类型:

typedef list_node<T> Node;

        接下来,我们开始实现list容器的常用接口。

交换两个容器的内容

        还是老套路,我们仅需交换它们的成员变量,就可以完成容器的交换。代码实现:

//交换两个链表
void swap(List<T> l)
{std::swap(_head, l.head);//交换头节点指针std::swap(_size, l._size);//交换size
}
//交换两个链表--非成员函数版
template <class T>
void swap(List<T>& l1, List<T>& l2)
{l1.swap(l2);//直接调用成员函数版
}

迭代器接口

         首先是普通迭代器和const迭代器的定义:

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

迭代器接口实现:

//迭代器接口
iterator begin()
{return iterator(_head->next);
}
iterator end()
{return _head;
}
const_iterator begin() const
{return iterator(_head->next);
}
const_iterator end() const
{return _head;
}

这里要注意一下它们的返回值:begin的返回值是指向首元素的迭代器,由于我们的链表头部有一个哨兵节点,所以首元素是哨兵节点的下一个节点;end的返回值是指向尾元素的“下一个元素”的迭代器,比较抽象,这里刚好可以定义为哨兵节点

空链表初始化

        该函数用于创建哨兵节点并且初始化链表的各项属性,便于后续构造函数调用。

//空链表的初始化
void empty_init()
{_head = new Node();_head->next = _head->prev = _head;_size = 0;
}

构造函数

        这里我们实现一下无参构造n个val值构造

//无参构造
List()
{empty_init();
}
//n个val值构造
List(size_t n, const T& val = T())
{empty_init();for (size_t i = 0; i < n; i++){push_back(val);//循环尾插}
}

拷贝构造函数

        拷贝构造函数的逻辑与n个val值构造相同,遍历源链表内容,循环尾插给当前链表即可。

//拷贝构造
List(const List<T>& l)
{empty_init();for (auto& e : l){push_back(e);}
}

赋值重载

        这里我们仍然使用新式写法,构造新对象然后交换,完成赋值操作。 代码如下:

//赋值重载
List<T>& operator=(List<T> l)
{swap(l);return *this;//支持连续赋值
}

析构函数

        对于析构函数,我们首先释放掉所有的节点空间,然后删除哨兵节点。

//析构
~List()
{clear();//调用clear函数清空链表delete _head;_head = nullptr;
}

容量接口size

        容量接口size用于获取链表数据个数。我们在函数当中返回成员变量_size的值。

//容量接口
size_t size()
{return _size;
}

插入和删除

        和之前实现vector时的逻辑相同,我们首先实现inserterase函数,这两个函数分别用于在任意位置进行插入和删除操作。然后,其他的插入和删除函数可以直接调用它们,能确保代码的高效性和复用性。

insert和erase

        insert的逻辑和我们c语言实现双向链表时大体相同(在pos位置之前插入),注意插入元素后,pos指向的不再是原本需要插入的位置,迭代器失效,所以函数要返回新插入的节点的迭代器。 

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

        erase负责删除pos指向的节点。注意删除节点之后,该节点的迭代器会失效,所以函数返回指向被删除节点的后一个节点的迭代器

iterator erase(iterator pos)
{assert(pos != end());//避免删除哨兵节点Node* del = pos._ptr;Node* next = del->_next;Node* prev = del->_prev;next->_prev = del->_prev;prev->_next = del->_next;delete del;_size--;return iterator(next);
}
push_back、push_front、pop_back、pop_front

尾插、头插、尾删、头删直接调用insert和erase函数即可。注意尾删节点时,end指向的前一个位置才是尾节点

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

清空链表

        清空链表的操作类似于我们c语言实现的销毁链表,这里循环调用erase函数来完成数据的清除。注意不要删除哨兵节点。 

//清空链表
void clear()
{auto it = begin();//从首元素开始,一个个删除while (it != end()){it = erase(it);}
}

三、程序全部代码

        关于list模拟实现的全部代码如下:

#include <iostream>
#include <cassert>
using namespace std;//节点
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){}
};//迭代器
template<class T, class Ref, class Ptr>
struct list_iterator
{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;Node* _ptr;//封装节点的指针//迭代器构造list_iterator(Node* node):_ptr(node){}//解引用Ref operator*(){return _ptr->_data;}//结构成员间接访问Ref operator->(){return &_ptr->_data;}//前置++Self& operator++(){_ptr = _ptr->next;return *this;}//前置--Self& operator--(){_ptr = _ptr->prev;return *this;}//后置++Self operator++(int){Self tmp(*this);_ptr = _ptr->next;return tmp;}//后置--Self operator--(int){Self tmp(*this);_ptr = _ptr->prev;return tmp;}//不等于bool operator!=(const Self& it){return _ptr != it._ptr;}
};//容器
template<class T>
class List
{typedef list_node<T> Node;
public:typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;//迭代器接口iterator begin(){return iterator(_head->next);}iterator end(){return _head;}const_iterator begin() const{return iterator(_head->next);}const_iterator end() const{return _head;}//空链表的初始化void empty_init(){_head = new Node();_head->next = _head->prev = _head;_size = 0;}//无参构造List(){empty_init();}//n个val值构造List(size_t n, const T& val = T()){empty_init();for (size_t i = 0; i < n; i++){push_back(val);//循环尾插}}//拷贝构造List(const List<T>& l){empty_init();for (auto& e : l){push_back(e);}}//赋值重载List<T>& operator=(List<T> l){swap(l);return *this;//支持连续赋值}//析构~List(){clear();//调用clear函数清空链表delete _head;_head = nullptr;}//容量接口size_t size(){return _size;}//插入和删除iterator insert(iterator pos, const T& x){Node* cur = pos._ptr;Node* newnode = new Node(x);newnode->_next = cur;newnode->_prev = cur->_prev;cur->_prev->_next = newnode;cur->_prev = newnode;_size++;return iterator(newnode);}iterator erase(iterator pos){assert(pos != end());//避免删除哨兵节点Node* del = pos._ptr;Node* next = del->_next;Node* prev = del->_prev;next->_prev = del->_prev;prev->_next = del->_next;delete del;_size--;return iterator(next);}void push_back(const T& x){insert(end(), x);}void push_front(const T& x){insert(begin(), x);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}//清空链表void clear(){auto it = begin();//从首元素开始,一个个删除while (it != end()){it = erase(it);}}//交换两个链表void swap(List<T> l){std::swap(_head, l.head);//交换头节点指针std::swap(_size, l._size);//交换size}
private:Node* _head;//头节点的指针size_t _size;//节点个数
};//交换两个链表--非成员函数版
template <class T>
void swap(List<T>& l1, List<T>& l2)
{l1.swap(l2);//直接调用成员函数版
}

总结

        本篇文章,我们在掌握list使用方法及其底层原理的基础上,模拟实现出了list容器。由于底层数据内存的非连续性,它的迭代器实现与vector、string有较大差异。之后博主会和大家一起开始stack和queue的学习。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

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

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

相关文章

SpringBoot:不支持发行版本17超详细解决办法

一开始linux中就已经下好了JDK21&#xff0c;但是后来创建项目的时候选用了JDK23&#xff0c;导致环境错乱&#xff0c;估计大部分都是因为这个原因&#xff0c;接下来我会一步步带大家解决。 检查系统环境&#xff08;以Ubuntu为例&#xff09; 没有下载JDK的可以在官网下载…

计算机网络中的数据包传输机制详解

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 计算机网络中的数据包传输机制详解 计算机网络中的数据包传输机制详解 计算机网络中的数据包传输机制详解 引言 数据包的基本概念…

Springboot3.3.5 启动流程之 tomcat启动流程介绍

在文章 Springboot3.3.5 启动流程&#xff08;源码分析&#xff09; 中讲到 应用上下文&#xff08;applicationContext&#xff09;刷新(refresh)时使用模板方法 onRefresh 创建了 Web Server. 本文将详细介绍 ServletWebServer — Embedded tomcat 的启动流程。 首先&…

HarmonyOs鸿蒙开发实战(9)=>解析json数据,自动生成实体Bean插件-jsonFormat使用教程(铁粉福利)

1.条件:基于HarmonyOs5.0.0版本. 2.老规矩先看效果> 3.第一步 >下载jsonFormat.jar文件,使用版本1.0.5-deveco https://plugins.jetbrains.com/plugin/24930-jsonformat/versions/stable 4.第二步 > 在DevEco Stuio中安装插件 5.第三步 > 新建bean文件&#xff…

VSCode+ESP-IDF开发ESP32-S3-DevKitC-1(2)第一个工程 LED心跳灯

VSCodeESP-IDF开发ESP32-S3-DevKitC-1&#xff08;2&#xff09;第一个工程 LED心跳灯 前言1.新建工程2.编写控制LED代码3.LED控制独立成.c和.h文件 前言 实际开发中很多时候我们需要有一个类似心跳灯或运行指示灯的灯以不同的状态闪烁以表示程序的运行状态&#xff0c;所以第…

系统掌握大语言模型提示词 - 从理论到实践

以下是我目前的一些主要个人标签&#xff1a; 6 年多头部大厂软件开发经验&#xff1b;1 年多 AI 业务应用经验&#xff0c;拥有丰富的业务提示词调优经验和模型微调经验。信仰 AGI&#xff0c;已经将 AI 通过自定义 Chatbot /搭建 Agent 融合到我的工作流中。头部大厂技术大学…

FromData格式提交接口时入参被转成JSON格式问题

本地上传文件后通过事件提交文件&#xff0c;一般先通过前端组件生成文本流&#xff0c;在通过接口提交文本流&#xff0c;提交文本流一般使用FormData的入参形式传入&#xff0c;接口请求头也默认"Content-Type": “multipart/form-data”&#xff0c;但是某些场景统…

【插件】重复执行 pytest-repeat

安装 pip3 install pytest-repeat 用法 1.命令行 pytest --count num pytest --count 32.装饰器 pytest.mark.repeat(num) #num运行次数 pytest.mark.repeat(5)#执行结果如下&#xff1a;

【Spring】循环引用 解决流程,只用一二级缓存?

文章目录 循环引用循环引用循环引用解决流程为什么不只用一二级缓存&#xff1f;:red_circle: 循环引用 循环引用 循环依赖&#xff1a;循环依赖其实就是循环引用&#xff0c;也就是bean互相持有对方&#xff0c;最终形成闭环。比如A依赖于B&#xff0c;B依赖于A 循环依赖在…

【青牛科技】视频监控器应用

1、简介&#xff1a; 我司安防产品广泛应用在视频监控器上&#xff0c;产品具有性能优良&#xff0c;可 靠性高等特点。 2、图示&#xff1a; 实物图如下&#xff1a; 3、具体应用&#xff1a; 标题&#xff1a;视频监控器应用 简介&#xff1a;视频监控器工作原理是光&#x…

机器学习day5-随机森林和线性代数1最小二乘法

十 集成学习方法之随机森林 集成学习的基本思想就是将多个分类器组合&#xff0c;从而实现一个预测效果更好的集成分类器。大致可以分为&#xff1a;Bagging&#xff0c;Boosting 和 Stacking 三大类型。 &#xff08;1&#xff09;每次有放回地从训练集中取出 n 个训练样本&…

5G与4G互通的桥梁:N26接口

5G的商用部署进程将是一个基于4G系统进行的长期的替换、升级、迭代的过程&#xff0c;4G系统是在过渡到5G全覆盖过程中&#xff0c;作为保障用户业务连续性体验这一目的的最好补充。 因此4G/5G融合组网&#xff0c;以及互操作技术将是各大运营商在网络演进中需要重点考虑的问题…

统信UOS开发环境支持Golang

UOS为Golang开发者提供了各种编辑器和工具链的支持,助力开发者实现高质量应用的开发。 文章目录 一、环境部署Golang开发环境安装二、代码示例Golang开发案例三、常见问题1. 包导入错误2. 系统资源限制一、环境部署 Golang开发环境安装 golang开发环境安装步骤如下: 1)安装…

【c++丨STL】list的使用

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;C、STL 目录 前言 list简介 一、list的默认成员函数 构造函数(constructor) 析构函数 赋值重载 二、list的迭代器接口 迭代器的功能分类 三、list的容量…

如何解决JAVA程序通过obloader并发导数导致系统夯住的问题 | OceanBase 运维实践

案例背景 某保险机构客户的数据中台&#xff0c;自系统上线后不久&#xff0c;会定期的用 obload 工具从上游业务系统导入数据至OceanBase数据库。但&#xff0c;不久便遇到了应用服务器的 Memory 与 CPU 资源占用持续攀升&#xff0c;最终导致系统夯住而不可用的异常。 memo…

人工智能:塑造未来的工作与生活

目录 人工智能技术的应用前景与影响 人工智能的历史与现状 人工智能的应用领域 人工智能的前景与挑战 个人视角&#xff1a;人工智能的应用前景与未来 人工智能在生活中的潜力 面对人工智能带来的挑战 我的观点与建议 结语 人工智能技术的应用前景与影响 随着人工智能…

MATLAB绘制克莱因瓶

MATLAB绘制克莱因瓶 clc;close all;clear all;warning off;% clear all rand(seed, 100); randn(seed, 100); format long g;% Parameters u_range linspace(0, 2*pi, 100); v_range linspace(0, pi, 50); [U, V] meshgrid(u_range, v_range);% Parametric equations for t…

go-zero(二) api语法和goctl应用

go-zero api语法和goctl应用 在实际开发中&#xff0c;我们更倾向于使用 goctl 来快速生成代码。 goctl 可以根据 api快速生成代码模板&#xff0c;包括模型、逻辑、处理器、路由等&#xff0c;大幅提高开发效率。 一、构建api demo 现在我们通过 goctl 创建一个最小化的 HT…

鸿蒙原生应用开发元服务 元服务是什么?和App的关系?(保姆级步骤)

元服务是什么&#xff1f;和App的关系&#xff1f; 元服务是是一种HarmonyOS轻量应用形态&#xff0c;用户无需安装即可使用&#xff0c;具备随处可及、服务直达、自由流转的特征。 元服务是可以独立部署和运行的程序实体&#xff0c;独立于应用&#xff0c;不依赖应用可独立…

k8s上部署redis高可用集群

介绍&#xff1a; Redis Cluster通过分片&#xff08;sharding&#xff09;来实现数据的分布式存储&#xff0c;每个master节点都负责一部分数据槽&#xff08;slot&#xff09;。 当一个master节点出现故障时&#xff0c;Redis Cluster能够自动将故障节点的数据槽转移到其他健…