【C++初阶】STL详解(八)List的模拟实现
本专栏内容为:C++学习专栏,分为初阶和进阶两部分。 通过本专栏的深入学习,你可以了解并掌握C++。
💓博主csdn个人主页:小小unicorn
⏩专栏分类:C++
🚚代码仓库:小小unicorn的代码仓库🚚
🌹🌹🌹关注我带你学习编程知识
STL详解(八)
- list的再认识:
- 初始化与定义节点:
- 迭代器实现:
- 构造:
- ++
- 解引用:*
- !=
- 基本框架搭建:
- --
- 后置++与后置--
- ->
- ==
- const迭代器
- 拓展:
- 拓展2:
- 相关函数接口:
- Insert:
- erase:
- push_front与pop_fronr
- push_back与pop_back
- size:
- clear与析构:
- 拷贝构造:
- 赋值重载:
- 传统写法:
- 现代写法:
- 对比vector与list
list的再认识:
在之前List的介绍与使用中,我们知道list容器是一个带头双向循环链表,那么我们在模拟实现中,能不能先证明一下List是否是一个双向循环链表呢?
我们参考一下stl中list的源码:

我们看到,在源码中,list中有一个__list_node的节点,我们将这个链表的节点定义打开发现定义两个指针next,prev.
再来看一下它的空初始化:

通过观察源码中list的初始化,确实是一个双向循环链表。
接下来。我们就来自己实现一下里面的接口函数。
注意:在模拟实现中,我们采取用与与源码中相同的命名风格。
为防止与库里面的list重复,我们模拟实现将定义在自己的命名空间中。
初始化与定义节点:
首先,我们需要定义三个类,并用摸版进行封装:分别是list,list的节点,以及迭代器:
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)
{}
};
list:
template<class T>
class list
{typedef list_node<T> Node;
public://空初始化:void empty_init(){_head = new Node;_head->_next = _head;_head->_prev = _head;}//无参构造:list(){empty_init();}void push_Back(const T& x){Node* tail = _head->_prev;Node* newnode = new Node(x);tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;}
private:Node* _head;
};
这里我们写的是无参构造,以及实现了一个尾插接口:
尾插双向链表实现已经再简单不过了:

现在我们测试一下:

现在还不能进行遍历,因此我们自己要实现一个迭代器:
迭代器实现:
那么这个迭代器我们要作为怎么实现呢?
我们可以先回顾一下,在vector中,我们实现迭代器就是实现原生指针。

在vector中,给it解引用就可以访问到里面的数据,但是链表不行,因为链表中空间不是连续的。
那么应该怎么实现呢?其实这个就和我们之前的日期类一样,在日期类中我们用运算符重载与封装实现了对日期类的++操作。而我们的迭代器也使这样实现的。
这里我们需要实现迭代器的!=,*与++操作:

我们先看一下库里面的操作:

构造:
看一下库里面的操作:

库里面用了一个节点的指针进行构造,这是因为:单参数的构造函数支持隐式类型转换。
所以我们的构造就可以这样写:
__list_iterator(Node* node):_node(node)
{
}
++
实现迭代器++,就是指针往后走的过程,注意返回的是迭代器。我们可以将迭代器重命名一下:
typedef __list_iterator<T> self;
实现代码:
self& operator++()
{_node = _node->_next;return *this;
}
解引用:*
返回节点里面的数据即可:
T& operator*()
{return _node->_data;
}
!=
两个迭代器进行比较,实质两个指针比较。
//两个迭代器进行比较,两个指针比较
bool operator!=(const self& s)
{return _node != s._node;
}
基本框架搭建:
这样基本上迭代器的基本架子就完成了:
typedef __list_iterator<T> iterator;
iterator begin()
{return _head->_next;
}
iterator end()
{return _head;
}
在list中定义一下迭代器。迭代器开始位置就是返回哨兵位头节点的下一个位置,结束位置就是返回哨兵位的地址。
测试一下:
void test_list1(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);list<int>::iterator it = lt.begin();while (it != lt.end()){//*it += 20;cout << *it << " ";++it;}cout << endl;
}
测试结果:

有了迭代器就有范围for:
for (auto e : lt)
{cout << e << " ";
}
cout << endl;

总结:其实会发现就是在模拟指针,但他的底层细节很大。所以迭代器体现了封装的强势之处。封装屏蔽底层差异和实现细节,提供统一的访问修改遍历方式。这样我们就不用关注他的底层是什么.

举个例子:
set<int> s;s.insert(1);s.insert(3);s.insert(2);s.insert(4);set<int>::iterator sit = s.begin();while (sit != s.end()){cout << *sit << " ";++sit;}cout << endl;
}

这里的set就是树,我们也可以依然用这个迭代器进行遍历。
–
实现迭代器++,就是指针往前走的过程,注意返回的是迭代器。
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;
}
->
在讲->重载之前,先看一下这个示例:
struct AA
{AA(int a1 = 0, int a2 = 0):_a1(a1), _a2(a2){}int _a1;int _a2;
};
list<AA> lt;
lt.push_back(AA(1, 1));
lt.push_back(AA(2, 2));
lt.push_back(AA(3, 3));list<AA>::iterator it = lt.begin();
while (it != lt.end())
{cout << *it << endl;++it;
}
cout << endl;
在这里就访问不了,因为自定义类型不支持类型。
这里我们回顾一下之前的知识,对与内置类型的指针,我们可以采用*来进行解引用。对于自定义类型的指针,我们要用->来进行解引用。
int* p = new int;
*p = 1;AA* ptr = new AA;
ptr->_a1 = 1;
实现->
T* operator->()
{return &_node->_data;
}
==
两个迭代器进行比较,就是两个指针比较
bool operator==(const self& s)
{return _node == s._node;
}
const迭代器
在实现const迭代器之前,首先要知道一点,const迭代器是一个完全不一样的类,所以不能将非const迭代器前加const就变成const迭代器。

因此我们可以list类中在定义一个const迭代器:
typedef __list_const_iterator<T> const_iterator;
const_iterator begin() const
{return _head->_next;
}
const_iterator end() const
{return _head;
}
在单独实现一个const迭代器的类:
template<class T>
struct __list_const_iterator
{....
}
const迭代器基本的功能与非const迭代器相似,只有在解引用时不同:
// *it = 10
const T& operator*()
{return _node->_data;
}// it->a1 = 10
const T* operator->()
{return &_node->_data;
}
测试一下:
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;
}void test_list4()
{list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);print_list(lt);
}

但是这样实现,还是太冗余了,因为非const和const只有返回值不同,那么还有优化的空间吗?
我们看一下库里面的实现:

库里面定义只定义了同一个类模版的迭代器,只是这两个迭代器之间的摸版参数不同,实例化的参数不同,就是完全不一样的类型。也就是说把能靠模版实现的就写一份,让编译器搞。
所以我们可以将我们的迭代器进行进一步优化:
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;....
}
// T T& T*
// T cosnt T& const T*
template<class T, class Ref, class Ptr>
struct __list_iterator
{typedef list_node<T> Node;/*typedef __list_iterator<T> self;*/typedef __list_iterator<T, Ref, Ptr> self;Node* _node;...
}
到这里,我们的迭代器就全部实现完了。
拓展:
在刚才的测试函数中,有一个print_list函数,但是这个测试函数里面的数据我们给定的是int,那我要是其他类型的呢,这个函数又该如何修改呢?
其实很简单:我们加一个摸版参数即可:
template<typename T>
void print_list(const list<T>& lt)
{typename list<T>::const_iterator it = lt.begin();while (it != lt.end()){//*it = 10;cout << *it << " ";++it;}cout << endl;
}
测试一下:

注意:这里我们没有用class这个摸版参数,这是因为:
list是一个未实例化的类模板,编译器不能去他里面去找
编译器就无法确定:list::const_iterator是内嵌类型,还是静态成员变量
前面加一个typename就是告诉编译器,这里是一个类型,等list实例化后再去类里面去取
拓展2:
如果要是将刚才的类在改造一下呢?
比如:
我要打印以下内容:
vector<string> v;
v.push_back("222222222222222222222");
v.push_back("222222222222222222222");
v.push_back("222222222222222222222");
v.push_back("222222222222222222222");
这个函数对于我们的printf_list就不适用了,因为我们的print_list就只适用于链表。
这里我们就可以写一个容器(container)的打印函数:
template<typename Container>
void print_Container(const Container& con)
{typename Container::const_iterator it = con.begin();while (it != con.end()){cout << *it << " ";++it;}cout << endl;
}
测试结果:

总结一下:
摸版实现了泛型编程,而泛型编程的本质,就是本来我们干的活,交给了编译器。
相关函数接口:
有了迭代器的实现,我们就可以实现一下链表的相关接口:
Insert:
Insert:在pos位置之前插入:
iterator insert(iterator pos, const T& x)
{Node* cur = pos._node;Node* newnode = new Node(x);Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return iterator(newnode);
}
Insert迭代器不会产生失效的问题,因为没有扩容。
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和erase后,我们就可以服用来实现头插,头删,尾插,尾删。
push_front与pop_fronr
具体实现:
头插:
//头插
void push_front(const T& x)
{insert(begin(), x);
}
头删:
//头删
void pop_front()
{erase(begin());
}
push_back与pop_back
具体实现:
尾插:
void push_back(const T& x)
{insert(end(), x);
}
尾删:
//尾删
void pop_back()
{erase(--end());
}
size:
为了方便计算大小,我们还可以再实现一个函数:
size_t size()
{return _size;
}
clear与析构:
clear:清理空间,我们可以采取迭代器访问的方式,逐个将节点释放。
//清理空间:
void clear()
{iterator it = begin();while (it != end()){it = erase(it);}
}
析构:我们可以先清理空间,在将头节点释放即可。
//析构
~list()
{clear();delete _head;_head = nullptr;
}
拷贝构造:
我们可以采用范围for来进行拷贝构造:
//拷贝构造:
// lt2(lt1)
//list(const list<T>& lt)
list(list<T>& lt)
{empty_init();for (auto e : lt){push_back(e);}
}
赋值重载:
传统写法:
list<int>& operator=(const list<int>& lt)
{if (this != <){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);
}// lt3 = lt1
list<int>& operator=(list<int> lt)
{swap(lt);return *this;
}
对比vector与list

相关文章:
【C++初阶】STL详解(八)List的模拟实现
本专栏内容为:C学习专栏,分为初阶和进阶两部分。 通过本专栏的深入学习,你可以了解并掌握C。 💓博主csdn个人主页:小小unicorn ⏩专栏分类:C 🚚代码仓库:小小unicorn的代码仓库&…...
Day02嵌入式---按键控灯
一、简单介绍 按键控制灯开关是一种常见的嵌入式系统示例项目,它通常用于演示嵌入式系统的基本控制能力。该项目由一个或多个LED和一个按键组成。通过按下按键,可以控制LED的开关状态,从而实现灯的亮灭控制。 二、查看功能手册 2.1 查看硬件…...
Centos设置nginx开机自启动设置
Centos设置nginx开机自启动设置 要设置CentOS中的Nginx开机自启动,可以按照以下步骤进行操作: 首先,登录到CentOS服务器上,并以root用户或具有sudo权限的用户身份执行以下命令来安装Nginx(如果尚未安装)&a…...
拼接合并yuv序列转成mp4
ffmpeg需要用支持libx264的版本,如果需要,可以从这个网站下载编译支持libx264\x265的ffmpeg https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-6.1-essentials_build.7z #-*- coding:utf-8-*- import osif __name__ "__main__":# 1 输入…...
访谈 破风之人毛京波,选择难而正确的路
“无论是在燃油时代还是电动时代,我们所做的一切,只为回归纯粹的驾驶乐趣。”履新路特斯中国总裁整整一年的毛京波,从不放过任何一个展示路特斯品牌驾驭精神的机会。 11月17日,广州车展开幕首日,位于5.2馆的路特斯“冠…...
java实现一个简单的监听器
在 Java 中,我们可以通过实现监听器(Listener)模式来处理事件和回调。监听器模式是一种常见的设计模式,用于实现对象间的松耦合通信。本文将介绍如何在 Java 中实现一个简单的监听器。 步骤 以下是实现一个监听器的基本步骤&…...
用HALCON标定助手对相机进行标定
任务要求: 已知相机镜头焦距f为8mm,相机单个CCD像素在水平和竖直两个方向上的尺寸均为3.75微米,相机为普通透光镜头和面阵相机,对相机进行标定,测量相机的内外参数。 操作步骤: 1. 在HALCON中运行gen_ca…...
5 个适用于 Windows 的顶级免费数据恢复软件
对于计算机来说,最重要的是用户数据。除了您的数据之外,有关计算机的其他所有内容都是可替换的。这三个是数据丢失的最常见原因: 文件/文件夹删除丢失分区分区损坏 文件/文件夹删除 文件/文件夹删除是最常见的数据丢失类型。大多数时候&am…...
MySQL 批量插入记录报 Error 1390 (HY000)
文章目录 1.背景2.问题3.分批插入4.一次最多能插入多少条记录?5.什么是 Prepared Statement?参考文献 1.背景 Golang 后台服务使用 GORM 实现与 MySQL 的交互,在实现一个通过 Excel 导入数据的接口时,使用 Save 方法一次性插入大…...
线程池(用于处理Runnable任务或Callable任务)
一,线程池 二, 如何创建线程池 案例: //1,通过ThreadPoolExecuter创建一个线程池对象ExecutorService pool new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS,new LinkedBlockingQueue<>(4),Executors.defaultThreadFactory(),new Thr…...
MATLAB在信号系统中的应用
1.产生一个幅度为1, 基频为2Hz,占空比为50%的周期方波.要求画出图形。 在MATLAB中,函数square(w0*t, DUTY)产生基本频率为w0 (周期T2*pi/w0)、占空比DUTY (τ/T)*100的周期矩形波(方波),默认情况下占空比DUTY50。占空…...
Jenkins与Docker的自动化CI/CD流水线实践
Pipeline 有诸多优点,例如: 项目发布可视化,明确阶段,方便处理问题 一个Jenkins File文件管理整个项目生命周期 Jenkins File可以放到项目代码中版本管理 Jenkins管理界面 操作实例:Pipeline的简单使用 这里是比较…...
企业数字化转型的作用是什么?_光点科技
在当今快速变化的商业环境中,数字化转型已成为企业发展的重要策略。企业数字化转型指的是利用数字技术改造传统业务模式和管理方式,以提升效率、增强竞争力和创造新的增长机会。 提升运营效率:数字化转型通过引入自动化工具和智能系统&#x…...
css加载会造成阻塞吗??
前言 前几天面试问到了这个问题,当时这个答得不敢确定哈哈,虽然一面还是过了 现在再分析下这个,总结下,等下次遇到就能自信得回答,666 准备工作 为了完成本次测试,先来科普一下,如何利用chr…...
Java中的jvm——面试题+答案(JVM的高级概念和调优技巧,包括垃圾回收、内存分析、优化技术等)——第16期
涉及Java虚拟机(JVM)高级概念和调优技巧的面试题以及简要答案: 什么是JVM调优?有哪些常见的JVM调优参数? 答案: JVM调优是通过调整JVM的参数和配置,以提高Java应用程序的性能和稳定性。常见的JV…...
***Linux下Mysql的安装
以下是在Linux系统下安装MySQL的步骤: 1.访问MySQL官网下载页面(https://dev.mysql.com/downloads/mysql/),选择适合您Linux系统的版本进行下载。 2.下载完成后,解压缩文件并将其移动到/usr/local目录下:…...
Linux踩坑:arm下gcc编译添加 -Ox 优化后,程序无法正常运行
arm下gcc编译添加 -Ox 优化后,程序无法正常运行 一、问题描述 今天学习正点原子的阿尔法开发板裸机开发的时候,遇到了一个问题,在没有使用 -Ox 优化的时候,编译出来的程序能够正常运行,但是添加了-Ox之后,…...
Vue3中Composition API介绍
在Vue 3中,引入了Composition API,它是一种新的组合式函数API,用于更灵活地组织和重用组件逻辑。Composition API相比于Vue 2中的Options API,提供了更好的可组合性和代码复用性。下面是对Vue 3中Composition API的介绍和用法&…...
虚拟机系列:(VMware Workstation Pro)Centos7下搭建Android开发环境及Android真机调试
一、Android SDK 安装配置 1、环境 Linux系统为:Red Hat Enterprise Linux 7 64 位 ; 当然还需要Java环境,java 环境这里不叙述; 2、Android Studio 安装 (1)下载位置: http://www.android-studio.org/ 我这里下载的:android-studio-ide-191.5977832-linux.tar.gz …...
全面(16万字)深入探索深度学习:基础原理到经典模型网络的全面解析
前言 Stacking(堆叠) 网页调试 学习率:它决定了模型在每一次迭代中更新参数的幅度激活函数-更加详细 激活函数的意义: 激活函数主要是让模型具有非线性数据拟合的能力,也就是能够对非线性数据进行分割/建模 如果没有激活函数: 第一个隐层: l…...
业务系统对接大模型的基础方案:架构设计与关键步骤
业务系统对接大模型:架构设计与关键步骤 在当今数字化转型的浪潮中,大语言模型(LLM)已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中,不仅可以优化用户体验,还能为业务决策提供…...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...
C++实现分布式网络通信框架RPC(3)--rpc调用端
目录 一、前言 二、UserServiceRpc_Stub 三、 CallMethod方法的重写 头文件 实现 四、rpc调用端的调用 实现 五、 google::protobuf::RpcController *controller 头文件 实现 六、总结 一、前言 在前边的文章中,我们已经大致实现了rpc服务端的各项功能代…...
华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...
Neo4j 集群管理:原理、技术与最佳实践深度解析
Neo4j 的集群技术是其企业级高可用性、可扩展性和容错能力的核心。通过深入分析官方文档,本文将系统阐述其集群管理的核心原理、关键技术、实用技巧和行业最佳实践。 Neo4j 的 Causal Clustering 架构提供了一个强大而灵活的基石,用于构建高可用、可扩展且一致的图数据库服务…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
如何在最短时间内提升打ctf(web)的水平?
刚刚刷完2遍 bugku 的 web 题,前来答题。 每个人对刷题理解是不同,有的人是看了writeup就等于刷了,有的人是收藏了writeup就等于刷了,有的人是跟着writeup做了一遍就等于刷了,还有的人是独立思考做了一遍就等于刷了。…...
Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信
文章目录 Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket(服务端和客户端都要)2. 绑定本地地址和端口&#x…...
《C++ 模板》
目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板,就像一个模具,里面可以将不同类型的材料做成一个形状,其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式:templa…...
