【C++】 初阶篇 STL list的模拟实现

一、list的模拟实现

1.1list的节点结构、容器结构、迭代器结构

我们需要自己写一个list,首先在list类外,还需要两个类,一个节点,一个迭代器。节点类和迭代器类为list类服务。(ps:说实话list部分真的还挺难写的,我自己给绕晕了好几次,主要在多了结构和迭代器都比较难,但是跨过这两个就会好很多了)

我们先从小的开始。

  • 节点结构
//节点类template<classT>structlist_node{typedeflist_node<T>Node;// 节点类型别名list_node();~list_node();T _val;// 节点数据域Node*_prev;// 后继节点指针Node*_next;// 前驱节点指针};

这个节点可以存放任何类型的数据,所以它得是一个模板。list_node有三部分构成:数据,指向下一个节点的指针,指向上一个节点的指针。

  • 迭代器结构

因为我们在上一篇文章中说道他的++、--、*不再是原生指针了,所以我们要对他进行封装。

这段迭代器结构目前还不完整,后面的讲解会逐步完善链表迭代器的实现:

//迭代器类template<classT>struct_list_iterator{typedeflist_node<T>Node;// 节点类型别名_list_iterator();~_list_iterator();Node*_node;// 指向链表节点的指针};
  • 容器结构
//链表类template<classT>classlist{typedeflist_node<T>Node;// 节点类型别名public:list();~list();private:Node*_head;size_t _size;};
  • 链表的迭代器为啥不能直接用原生指针?

因为链表的原生指针(比如list_node*)只能访问节点本身,而迭代器需要模拟 “像普通指针一样解引用取数据、++/--遍历” 的行为 —— 链表节点里存的是_val(实际数据),原生指针解引用得到的是整个节点,不是数据;且链表的 “下一个元素” 需要通过_next指针跳转,原生指针的++是地址 + 1(不符合链表的节点连接逻辑),所以得封装迭代器类来重载*、++等运算符,不能直接用原生指针。

  • 迭代器结构为啥用struct?

迭代器结构体用struct,是因为迭代器只是遍历容器的工具 —— 哪怕直接定义迭代器对象,没有对应的容器支撑,也没法实际访问有效数据,所以不用刻意封装成私有,用struct让成员(比如这里的_node指针)直接暴露,能简化后续迭代器功能的实现。

  • 迭代器为啥不能写析构函数?

不能为迭代器编写析构函数 —— 因为节点的内存是由容器管理的,迭代器只是 “借用节点指针来访问元素”,本身并不持有节点的所有权。若在迭代器析构时释放节点,会导致容器内的节点被非法销毁,进而引发内存错误。

1.2list的默认函数

  • list构造函数
//list构造函数list(){_head=newNode;_head->_prev=_head;// 头节点前驱指向自身(循环链表)_head->_next=_head;// 头节点后继指向自身(循环链表)_size=0;// 链表初始长度为0}
  • list拷贝构造函数
// list拷贝构造函数list(constlist<T>&lt){_head=newNode;//先创建一个头结点_head->_prev=_head;// 头节点前驱指向自身(循环链表)_head->_next=_head;// 头节点后继指向自身(循环链表)_size=0;// 链表初始长度为0//遍历lt的每个元素,逐个尾插到新链表中,这个要支持迭代器for(auto&e:lt){push_back(e);}}

push_back这个函数我们后面实现。

  • list赋值运算符重载
//list的交换函数voidswap(list<T>&lt){std::swap(_head,lt._head);std::swap(_size,lt._size);}//list赋值运算符重载//已经有了实体// l1=l2list<T>&operator=(list<T>lt)// 传值调用,自动拷贝出临时对象lt{swap(lt);// 交换当前对象与临时对象的资源return*this;// 返回当前对象,临时对象会自动销毁旧资源}
  • list析构函数
// 清空链表中所有有效元素(保留头节点)voidclear(){iterator it=begin();// 获取链表起始迭代器while(it!=end())// 遍历所有有效元素{it=erase(it);// 删除当前元素,erase返回下一个元素的迭代器}_size=0;// 重置有效元素个数为0}// 链表析构函数:释放所有资源~list(){clear();// 先清空所有有效元素delete_head;// 释放头节点的堆内存_head=nullptr;// 将头节点指针置空,避免野指针}

二、迭代器结构(重点)

//迭代器结构体template<classT>struct_list_iterator{typedeflist_node<T>Node;// 节点类型别名typedef_list_iterator<T>iterator;//迭代器类型别名// 构造:用节点指针初始化迭代器_list_iterator(Node*node):_node(node){}Node*_node;// 指向链表节点的指针};

(迭代器本身就是一个封装指针,所以不需要拷贝构造,浅拷贝够用)

我们已经写出来的接下来我们要实现让他可以像指针一样使用。

2.1operator*

原本的指针解引用返回具体内置类型值,但是这里的链表节点时一个结构体,所以必须重载解引用。如下代码,我们让解引用返回的是结构体内部的数据。

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

2.2operator++和--

分别包装了向前找一个节点返回和向后找一个节点返回的逻辑。

iterator&operator++(){_node=_node->_next;return*this;}iteratoroperator++(int){iterator tmp=*this;_node=_node->_next;returntmp;}iterator&operator--(){_node=_node->_prev;return*this;}iteratoroperator--(int){iterator tmp=*this;_node=_node->_prev;returntmp;}

前置++和--的逻辑很相似:都是直接修改当前迭代器的节点指针(_node),然后返回自身的引用。

比如前置++是把_node指向 “下一个节点”,前置--是指向 “前一个节点”;返回自身引用的好处是支持链式操作(比如++(++it)、--(--it)),而且没有临时对象的开销,效率更高。

后置++和--的核心是 “先返回原状态,再移动”:

函数参数里的int是个占位符(没有实际意义,只是用来区分前置 / 后置),实现时会先创建一个临时迭代器tmp保存当前状态,然后移动_node到下一个 / 前一个节点,最后返回这个临时迭代器。

这样外部使用时,it++拿到的是 “移动前的迭代器”,而it本身已经完成了移动 —— 不过因为会创建临时对象,它的效率比前置版本略低。

2.3operator==与!=

相等与不相等的判断依据固定在对于节点的比较。

booloperator==(constiterator&it)const{return_node==it._node;}booloperator!=(constiterator&it)const{return_node!=it._node;}

这两个运算符的逻辑很直接:判断两个迭代器的_node指针是否指向同一个节点。

operator!=返回_node != it._node,表示 “两个迭代器是否指向不同节点”;

operator==返回_node == it._node,表示 “两个迭代器是否指向同一个节点”。其中operator!=后面加了const,是为了保证 “调用这个函数时不会修改当前迭代器的状态”,更符合 const 正确性的规范。

2.4operator->

重载这个运算符,是为了应对数据是结构体的场景:当存储的数据是结构体时,仅用operator*解引用后,没办法直接访问结构体的成员(得通过.来访问);而重载->后,就能直接通过迭代器用->访问结构体成员,用起来更便捷。

但这里又有个问题:我们通过operator->拿到的是_node节点的指针,而不是节点里存储的结构体成员 —— 这显然不是我们想要的访问效果。

我们看这段代码的实际效果:当用迭代器it访问结构体A的成员时,理论上应该写it->->_a2(因为operator->()返回的是A*指针,需要再用->访问成员),但编译器做了特殊处理 —— 自动省略了一个->,所以直接写it->_a1/it->_a2就能正常访问。

这背后的逻辑是:operator*()返回的是A&引用(所以可以用(*it)._a1访问),而operator->()返回的是A*指针;为了让迭代器的用法和原生指针一致(原生指针可以直接用->访问成员),C++ 编译器对迭代器的operator->做了 “语法糖” 优化,允许省略一次->,让it->_a2等价于(it.operator->())->_a2,用起来更自然简洁。

T*operator->(){return&(_node->_val);//c++会多处理一次所以返回地址即可}

2.5begin、end

// 普通正向迭代器:指向第一个有效元素iteratorbegin(){returniterator(_head->_next);}// 普通正向迭代器:指向尾后位置(头节点)iteratorend(){returniterator(_head);}

三、更高维度的范式编程

首先我们知道有const的迭代器,那么我们要是再写一个const迭代器的类吗太麻烦了,所以 STL 库引入了 3 个参数来解决。

// 链表迭代器模板类(T:元素类型 Ref:元素引用类型 Ptr:元素指针类型)template<classT,classRef,classPtr>

ok,我们看一下为什么会这样

那么我们先修改一下之前的代码

//迭代器类template<classT,classRef,classPtr>struct_list_iterator{typedeflist_node<T>Node;// 节点类型别名typedef_list_iterator<T,Ref,Ptr>self;// 迭代器自身类型别名,简化后续使用_list_iterator(Node*node):_node(node){}self&operator++(){_node=_node->_next;return*this;}selfoperator++(int){self tmp=*this;_node=_node->_next;returntmp;}self&operator--(){_node=_node->_prev;return*this;}selfoperator--(int){self tmp=*this;_node=_node->_prev;returntmp;}Refoperator*(){return_node->_val;}booloperator==(constself&it)const{return_node==it._node;}booloperator!=(constself&it)const{return_node!=it._node;}Ptroperator->(){return&(_node->_val);//c++会多处理一次所以返回地址即可}Node*_node;// 指向链表节点的指针};

Ptr、Ref模板参数的作用?

Ptr的作用在此体现:区分普通迭代器返回的T*(可读写结构体成员)和const迭代器返回的const T*(只读),实现一套模板复用,保证const迭代器的安全性。

至于为啥要将迭代器重命名为self,是因为它的完整类型名太长了,用self代替能简化代码书写。

四、list增删查改

4.1push_back

voidpush_back(constT&x){Node*newnode=newNode(x);Node*tail=_head->_prev;tail->_next=newnode;newnode->_prev=tail;_head->_prev=newnode;newnode->_next=_head;++_size;}

先找到链表的尾节点tail(通过头节点_head的_prev拿到),再创建新节点newnode;

接着把tail的_next指向新节点、新节点的_prev指向tail,完成新节点和原尾节点的连接;

最后让新节点的_next指向头节点、头节点的_prev指向新节点,维持链表的循环结构 —— 这样新节点就成了新的尾节点,尾插完成。

4.2insert

// pos位置之前插入iteratorinsert(iterator pos,constT&x){Node*cur=pos._node;//迭代器指向的节点Node*prev=cur->_prev;Node*newnode=newNode(x);prev->_next=newnode;newnode->_next=cur;cur->_prev=newnode;newnode->_prev=prev;++_size;returniterator(newnode);//返回新节点的迭代器}

先从迭代器pos里拿到它指向的节点cur,再找到cur的前驱节点prev,同时创建新节点newnode;

把prev的_next指向新节点、新节点的_next指向cur,再让cur的_prev指向新节点、新节点的_prev指向prev—— 这样就把新节点 “夹” 在了prev和cur之间;

最后更新链表长度_size,并返回新节点对应的迭代器,方便后续操作。

4.3push_front

voidpush_front(constT&x){insert(begin(),x);}

直接复用insert,在begin()(头节点迭代器)前插入元素x,既简化代码又保证逻辑一致。

4.4pop_back

voidpop_back(){// 断言:确保链表不为空(避免删除空链表的元素)assert(_head->_next!=_head);// 找到要删除的尾节点Node*del=_head->_prev;// 找到尾节点的前驱节点(新的尾节点)Node*tail=del->_prev;// 建立新尾节点和头节点的双向连接,断开原尾节点tail->_next=_head;_head->_prev=tail;// 释放原尾节点的内存deletedel;// 更新链表元素个数_size--;}

先断言链表非空,找到尾节点和它的前驱节点,重新建立前驱节点与头节点的循环连接,释放原尾节点内存,最后更新链表长度。

4.5erase

// pos位置删除iteratorerase(iterator pos){// 断言:确保删除的不是尾后迭代器(无效节点)assert(pos!=end());// 提取待删除节点curNode*cur=pos._node;// 找到cur的前驱和后继节点Node*prev=cur->_prev;Node*next=cur->_next;// 建立前驱和后继的双向连接,跳过待删除节点prev->_next=next;next->_prev=prev;// 释放待删除节点内存deletecur;// 更新链表元素个数--_size;// 返回后继节点迭代器,避免迭代器失效returniterator(next);}

先断言避免删除无效节点,找到待删节点的前后节点并重新建立连接,释放待删节点内存,更新链表长度后返回后继节点迭代器防止失效。

4.6pop_front

voidpop_front(){// 复用erase函数,删除头节点(begin()对应的迭代器)erase(begin());}

直接复用erase函数,删除begin()对应的头节点,既简化代码又保证逻辑一致。

4.7clear

// 清空链表中所有有效元素(保留头节点)voidclear(){iterator it=begin();// 获取链表起始迭代器while(it!=end())// 遍历所有有效元素{it=erase(it);// 删除当前元素,erase返回下一个元素的迭代器}_size=0;// 重置有效元素个数为0}

4.8empty

boolempty()const{return_size==0;}

相关新闻

【C++】 初阶篇 STL list的使用

【C++】 初阶篇 STL list的使用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 2:15:49 阅读更多 →
车间班组标准化作业提质体系

车间班组标准化作业提质体系

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 2:14:49 阅读更多 →
ACORN算法的各种密码分析方法全面盘点

ACORN算法的各种密码分析方法全面盘点

ACORN算法的各种密码分析方法全面盘点ACORN算法作为CAESAR竞赛的最终入选算法之一&#xff0c;自提出以来受到了广泛的密码分析。目前针对它的密码分析方法已相当全面&#xff0c;涵盖了从数学结构到物理实现的多个层面。需要说明的是&#xff0c;现有攻击主要针对简化轮数的AC…

2026/10/21 2:14:49 阅读更多 →

最新新闻

思科模拟器6.2:从安装到避坑的完整实战指南

思科模拟器6.2:从安装到避坑的完整实战指南

简介&#xff1a;这是一份思科模拟器6.2安装包&#xff0c;官方名称为Packet Tracer&#xff0c;面向网络技术学习者、思科认证考生以及开展网络实验教学的教师。软件以图形化方式搭建虚拟网络环境&#xff0c;可模拟路由器、交换机、无线接入点、防火墙等设备&#xff0c;支持…

2026/10/21 2:47:00 阅读更多 →
思科模拟器6.2全攻略:从网络仿真到CCNA实验与排错

思科模拟器6.2全攻略:从网络仿真到CCNA实验与排错

简介&#xff1a;网络仿真工具是网络工程师和认证学习者不可或缺的实践平台&#xff0c;它通过软件模拟真实设备的行为&#xff0c;让复杂的协议交互和故障排查变得可视化、可重复。思科模拟器作为经典的网络仿真环境&#xff0c;不仅支持路由、交换、无线等基础实验&#xff0…

2026/10/21 2:47:00 阅读更多 →
iOS地图定位Demo:原生MapKit+CoreLocation从权限到落图

iOS地图定位Demo:原生MapKit+CoreLocation从权限到落图

简介&#xff1a;一个面向iOS开发者的地图定位入门Demo&#xff0c;演示Core Location与MapKit的集成方式&#xff0c;清晰解决定位权限申请、位置获取、地图展示等基础问题&#xff0c;适合初学者到中级开发者作为实践参考。压缩包共23个文件&#xff0c;主要包含Objective-C源…

2026/10/21 2:47:00 阅读更多 →
拆解一个做 PPT 工具:素材自动变大纲一键出片

拆解一个做 PPT 工具:素材自动变大纲一键出片

## 背景这篇记录一次把做 PPT流程工程化的尝试。## 核心能力- 文本素材自动梳理大纲- 大纲支持二次编辑- 多套配色模板可选- 渲染16:9标准PPTX文件## 落地场景学习提升等场景都能直接搬进工作流&#xff0c;输入是散乱的原始材料&#xff0c;输出是可直接交付的成品。## 实现要…

2026/10/21 2:47:00 阅读更多 →
Git核心操作与四区模型详解:从基础命令到协作实战避坑指南

Git核心操作与四区模型详解:从基础命令到协作实战避坑指南

1. 为什么“git 基本操作”值得单独拎出来讲刚入行那会儿&#xff0c;我对 Git 的态度就是“能用就行”——clone、pull、push三板斧走天下&#xff0c;遇到冲突就慌&#xff0c;遇到回退就懵&#xff0c;实在不行就把整个项目删了重新拉一遍。后来参与协作的项目多了&#xff…

2026/10/21 2:47:00 阅读更多 →
Hurys毫米波雷达CAN调试软件安装与通信故障排查指南

Hurys毫米波雷达CAN调试软件安装与通信故障排查指南

简介&#xff1a;本资源是面向汽车电子工程师与ADAS系统开发者的毫米波雷达调试工具集&#xff0c;聚焦Hurys雷达参数配置、CAN通信联调与故障诊断等核心场景&#xff0c;解决雷达实车测试中软硬件协同调试难、驱动兼容性差、数据解析门槛高等实际问题。压缩包共19个文件&#…

2026/10/21 2:46:00 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →