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;
}

相关文章:
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->…...
Elasticsearch:使用 Open AI 和 Langchain 的 RAG - Retrieval Augmented Generation (三)
这是继之前文章: Elasticsearch:使用 Open AI 和 Langchain 的 RAG - Retrieval Augmented Generation (一) Elasticsearch:使用 Open AI 和 Langchain 的 RAG - Retrieval Augmented Generation (二&…...
主流电商平台价格如何高频监测
双十一来临在即,除了商家很兴奋,品牌和消费者同样持续关注,除了关注不同平台的产品上架情况,价格也是这些渠道参与者最为关注的,品牌需要通过掌握各店铺的价格情况,了解市场情况以及各经销商的渠道治理现状…...
Spring关于注解的使用
目录 一、使用注解开发的前提 1.1 配置注解扫描路径 二、使用注解创建对象 2.1 Controller(控制器储存) 2.2 Service(服务储存) 2.3 Repository(仓库储存) 2.4 Component(组件储存) …...
图像处理入门 1(Introduction to image processing)
如何获得一张照片 (How to obtain a photo)? 每次看到一些光学设备的规格介绍的时候,一些专用名词,例如:等效焦距,曝光模式 等 让你一头雾水。爱学习的你一定十分好奇他们是什么意思。每次看到…...
中国大模型开源创新与合作的新篇章 | 2023 CCF中国开源大会
2023年10月21日至22日,由中国计算机学会(CCF)和开放原子开源基金会联合主办的CCF中国开源大会(CCF ChinaOSC)在湖南省长沙市北辰国际会议中心成功召开。此次大会以“开源联合,聚力共赢”为主题,…...
vue项目切换页面白屏的解决方案
问题描述 1、页面切换后白屏,同时切换回上一个页面同样白屏 2、刷新后正常显示 3、有警告:Component inside <Transition> renders non-element root node that cannot be animated 解决方法 <Transition>中的组件呈现不能动画化的非元素根…...
5G vs 4G
5G与4G的关键性能指标对比 指标名称流量密度连接密度空口时延移动性能效指标用户体验速率频谱效率峰值速率4G 参考值0.1 M b i t / s / m 2 Mbit/s/m^2 Mbit/s/m2 1 ∗ 1 0 5 / k m 2 1*10^5/km^2 1∗105/km210ms350km/h1倍10Mbit/s1倍1Gbit/s5G 参考值10 M b i t / s / m 2 M…...
【JavaEE重点知识归纳】第11节:认识异常
目录 一:异常的概念和体系结构 1.概念 2.体系结构 3.异常分类 二:异常的处理 1.防御式编程 2.异常的抛出 3.异常的捕获 4.异常的处理流程 三:自定义异常 一:异常的概念和体系结构 1.概念 (1)在…...
一键自助建站系统api版系统源码
自助建站系统,一建建站系统api版,自动建站 安装推荐php7.2或7.2以下都行 可使用虚拟主机或者服务器进行搭建。 分站进入网站后台 域名/admin 初始账号123456qq.com密码123456 找到后台的网站设置 将主站域名及你在主站的通信secretId和通信secretKey填进去。 即可正常使用 通信…...
全国三维数字化创新设计大赛湖北赛区省赛成功举办
须弥芥子,数字如海。10月14日—15日,2023 年数字科技文化节——第16届全国三维数字化创新设计大赛湖北赛区省赛暨产教联合体大会在武汉软件工程职业学院成功举行。 (大赛全体专家领导合影) 全国三维数字化创新设计大赛组委会副秘…...
OpenCV #以图搜图:均值哈希算法(Average Hash Algorithm)原理与实验
1. 介绍 均值哈希算法(Average Hash Algorithm) 是哈希算法的一种,主要用来做相似图片的搜索工作。 2. 原理 均值哈希算法(aHash)首先将原图像缩小成一个固定大小的像素图像,然后将图像转换为灰度图像&am…...
博通BCM575系列RDMA网卡驱动bnxt_re分析(一)
简介 整个BCM系列驱动分成以太网部分(bnxt_en.ko)和RDMA部分(bnxt_re.ko), 两个模块之间通过内核的auxiliary_bus进行管理.我们主要分析下bnxt_re驱动. 代码结构 这个驱动的核心是 qplib_fp.c, 这个文件主要包含了驱动的数据路径, 包括Post Send, Post Recv, Poll CQ流程的实…...
集合总结-
Collection 常用方法 package com.test01;import java.util.ArrayList; import java.util.Collection; /*添加元素---boolean add(E e);移除元素---boolean remove(Object c);判断元素是否存在---boolean contains(Object c);*/ public class S {public static void main(Str…...
【知识串联】概率论中的值和量(随机变量/数字特征/参数估计)【考研向】【按概率论学习章节总结】
就我的概率论学习经验来看,这两个概念极易混淆,并且极为重点,然而,在概率论的前几章学习中,如果只是计算,对这方面的辨析不清并没有问题。然而,到了后面的参数估计部分,却可能出现问…...
上游服务不可用了,下游服务如何应对?
上游服务不可用了,下游服务如何应对? 引言 在系统中,上游服务和下游服务是两个关键概念。上游服务通常指的是提供某种功能或数据的服务端,它接收来自下游服务的请求,并根据请求进行处理和响应。下游服务通常指的是发…...
WebGL笔记:矩阵的变换之平移的实现
矩阵的变换 变换 变换有三种状态:平移、旋转、缩放。当我们变换一个图形时,实际上就是在移动这个图形的所有顶点。解释 webgl 要绘图的话,它是先定顶点的,就比如说我要画个三角形,那它会先把这三角形的三个顶点定出来…...
XTU-OJ 1187-Candy
WCB某天买了非常多的糖果并把它们分成N份,依次分别有1,2,3…,N个糖果。他想拿出其中的3份分给他的室友, 为了不让室友们闹意见,必须让这三份的糖果总数恰好能被三人均分。请问他一共有多少种不同的组合方案数ÿ…...
基于 nodejs+vue城市轨道交通线路查询系统mysql
目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性:…...
电商时代,VR全景如何解决实体店难做没流量?
近日,电商和实体经济的对立成为了热门话题,尽管电商的兴起确实对线下实体店造成了一定的冲击,但实体店也不是没有办法挽救。VR全景助力线下实体店打造线上店铺,打通流量全域布局,还能实现打开产品、查看产品内部细节等…...
基于算法竞赛的c++编程(28)结构体的进阶应用
结构体的嵌套与复杂数据组织 在C中,结构体可以嵌套使用,形成更复杂的数据结构。例如,可以通过嵌套结构体描述多层级数据关系: struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...
椭圆曲线密码学(ECC)
一、ECC算法概述 椭圆曲线密码学(Elliptic Curve Cryptography)是基于椭圆曲线数学理论的公钥密码系统,由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA,ECC在相同安全强度下密钥更短(256位ECC ≈ 3072位RSA…...
Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...
【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...
基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上,开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识,在 vs 2017 平台上,进行 ASP.NET 应用程序和简易网站的开发;初步熟悉开发一…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...
ABAP设计模式之---“简单设计原则(Simple Design)”
“Simple Design”(简单设计)是软件开发中的一个重要理念,倡导以最简单的方式实现软件功能,以确保代码清晰易懂、易维护,并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计,遵循“让事情保…...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...
Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...

