当前位置: 首页 > news >正文

C++STL详解(五)——list类的具体实现

一.本次所需实现的三个类及其成员函数接口

链表首先要有结点,因此我们需要实现一个结点类。

链表要有管理结点的结构,因此我们要有list类来管理结点。

链表中还要有迭代器,而迭代器的底层其实是指针。但是我们现有的结点类无法完成迭代器的行为,因此我们还需要实现一个迭代器类。

因此,我们要实现的三个类分别是:结点类、迭代器类、链表类。

namespace trousers
{template<class T>struct _list_node{//初始化_list_node();//变量T data;//数值域_list_node<T>* prev;//前驱指针_list_node<T>* next;//后继指针};//迭代器    //由于list_node无法遍历和支持迭代器,因此我们需要手动实现一个迭代器版本//Ref和Ptr分别代表引用和指针类型,我们可以用一个类模板重载出两个迭代器版本。(迭代器里面需要用到指针和引用),一个模板的话不够用template<class T, class Ref, class Ptr>struct _list_iterator{typedef _list_node<T> node;typedef _list_iterator<T,Ref,Ptr> self;self operator++();self operator++(int);self operator--();self operator--(int);Ref operator*();Ptr operator->();//_pnode->_val  operator->(*this)->_valself operator==(const self& t)constself operator!=(const self& t)const//变量node* _pnode;};//链表template <class T>class list{public:typedef _list_node<T> node;typedef _list_iterator<T,T&,T*> iterator;typedef _list_iterator<T, const T&, const T*> const_iterator;//默认成员函数list();list(const list<T>& lt);list<T>& operator=(const list<T>& lt);~list();//迭代器相关函数iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;//访问容器相关函数T& front();T& back();const T& front() const;const T& back() const;//插入、删除函数void insert(iterator pos, const T& x);iterator erase(iterator pos);void push_back(const T& x);void pop_back();void push_front(const T& x);void pop_front();//其他函数size_t size() const;void resize(size_t n, const T& val = T());void clear();bool empty() const;void swap(list<T>& lt);//私有变量private:node* _head;//指向哨兵位};
}

二.结点类的模拟实现

list的底层其实是一个带头双向循环链表。

因此,我们需要实现的结点类中需要的成员为:数据、前一个结点的指针、后一个结点的指针。

对于该类而言,我们不需要在类中完成任何行为,因此我们仅仅只需要实现一个构造函数即可。而该类由于都是内置成员,因此我们的析构函数可以由编译器生成。

2.1结点类的构造函数

结点类的构造函数实现起来是比较简单的,我们仅仅需要将val置想要的值,并将两个指针置空即可。

		_list_node(const T& data=T()):_data(),_prev(nullptr),_next(nullptr){}

三.迭代器类的模拟实现

3.1迭代器的设计思路

在实现了结点类之后,我们就要开始实现迭代器类了。

由于我们无法通过只有一个参数的类模板实现const和非const的两个迭代器

因此我们这里的类模板有三个参数。

	template<class T, class Ref, class Ptr>

其中的Ref表示引用,Ptr表示解引用。 

3.2迭代器类存在的意义

在之前实现string和vector的时候,我们都不需要实现一个迭代器类,为什么实现list的时候就需要实现一个迭代器类了呢?

这是因为,string和vector对象都将数据存储在了一块连续的内存空间,我们通过指针操纵空间从而完成自增、自减、解引用等操作,然后就可以对数据进行一系列的操作,因此string和vector当中的迭代器就是原生的指针。

 但是,对于list而言,各个结点在内存中的分布并不连续,因此我们不能通过对结点的自增自减等操作来完成迭代器的行为。

而迭代存在的意义就是,让使用者不必关心底层的实现,可以用简单统一的方式对容器内的数据进行访问。

 既然list的结点指针的行为不满足迭代器的定义,那么我们就需要对结点指针进行封装,对结点指针的各个运算符进行重载,使得其支持像vector、string中的迭代器一样的操作。

举个例子,我们在用list的自增行为时,实际上是执行了p=p->next语句。

总结:list的迭代器类,实际上只是对结点的指针进行了封装,并对其各个操作符进行了重载,使得结点指针的各种行为看起来和普通指针一样。

3.3构造函数

迭代器类的构造函数实际上是对结点指针进行了封装而已,其成员变量只有一个结点的指针,因此我们的构造函数直接根据所给的结点指针构造出一个迭代器对象即可。

		_list_iterator(node* pnode):_pnode(pnode){}

3.4++运算符的重载

3.4.1前置++操作符

对于前置++操作符,我们的实现思路非常简单,直接将结点指针指向next,然后返回自增后的结果即可。

		self operator++(){_pnode = _pnode->_next;return *this;}
3.4.2后置++操作符

对于后置++操作符,我们采取使用一个临时变量记录该结点的方式,对结点指针完成自增,并返回临时变量即可。

		self operator++(int){self tmp(*this);_pnode = _pnode->_next;return tmp;}

3.5--运算符的重载

--操作符的逻辑和++操作符的逻辑基本上是一样的,只不过是将自身修改为prev而不是next。

		self operator--(){_pnode = _pnode->_prev;return *this;}self operator--(int){self tmp(*this);_pnode = _pnode->_prev;return tmp;}

3.6==运算符的重载

在使用迭代器遍历时,难免会比较两个迭代器是否相同,因此我们还需要实现==操作符。

而两个迭代器是否相同,实际上就是判断这两个迭代器是不是同一个位置上的迭代器,因此,我们只需要比较这两个迭代器的地址即可。

		bool operator==(const self& t)const{return _pnode == t._pnode;}

3.7!=运算符的重载

!=操作符和==操作符的作用相反,我们要判断的是这两个迭代器的地址是不是不同。

		bool operator!=(const self& t)const{return _pnode != t._pnode;}

3.8*运算符的重载

当我们使用*操作符时,其实就是想要得到这个地址的数据,因此我们直接返回当前指针数据所指向的数据即可,但由于我们可能会通过解引用修改数据,因此我们这里可以返回引用。

		Ref operator*(){return _pnode->_data;}

3.9->运算符的重载

在一些场景下,我们可能还会使用到->操作符。

譬如如下场景:

当list容器内的每个结点存储的是自定义类型时,那么当我们拿到一个位置的迭代器,我们可能还会通过->操作符来访问该类型内部的成员。

如下例:

list<vector> d;
vector<int> v1 = { 1,2,3 };
vector<int> v2 = { 4,5,6 };
vector<int> v3 (3,5);
d.push_back(v1);
d.push_back(v2);
d.push_back(v3);
list <vector>::iterator pos = it.begin();
cout << pos->size() << endl;

因此,有些情况下我们会使用到->操作符。

对于->操作符的重载,我们直接返回结点当中所存储数据的地址即可。

		Ptr operator->()//_pnode->_val  operator->(this)->_val{//(*this)->_data;return &(_node->_data);}

说到这里,你可能会觉得有些不对,按照这种重载方式的话,我们似乎需要两个->才能调到我们想要的数据,也就是这样:

因为()内部的this是被省略的,因此我们实际上写应该是这样的

但是一个地方出现两个箭头的可读性有些过于差劲了,因此编译器在这里做了一些特殊的处理,省略了一个箭头,也就是我们所写的版本。

四.list的模拟实现

4.1默认成员函数

4.1.1构造函数

list是一个带头双向循环链表,因此在构造一个list对象时,直接申请一个头节点并让前驱指针和后继指针都指向自己即可。

		list(){_head = new node;_head->_next = _head;_head->_prev = _head;}
4.2.2拷贝构造函数

拷贝构造函数,我们需要先申请一个结点,然后申请一个头结点,之后我们再将原list链表一个一个通过尾插拷贝过去即可。

		list(const list<T>& lt){_head = new node;_head->_next = _head;_head->_prev = _head;for (const auto& e : lt){push_back(e);}}
4.2.3赋值运算符重载函数

赋值运算符重载和拷贝构造的实现方式是类似的,我们可以先将被赋值的链表清空然后一个一个通过尾插拷贝过去。

		list<T>& operator=(const list<T>& lt){if (this != &lt)//防止自己给自己赋值,以避免性能浪费{clear();for (const auto& e : lt){push_back(e);}}return *this;}

但是这种写法过于繁琐,我们也可以换一种思路,我们不采取引用传参,那么我们将会传入一个形参,之后我们和形参进行交换即可完成任务,而当我们的函数运行结束后还会自动销毁掉形参。

		list<T>& operator=(const list<T> lt)//编译器接受右值时自动调用其拷贝构造函数,构造出形参。{swap(lt);return *this;}
 4.2.4析构函数

对于析构函数,我们首先使用clear清理一下容器内的数据,然后将头结点释放,之后再将指针置空即可。

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

五.迭代器相关函数


begin和end

begin是返回第一个有效数据的迭代器,因此我们要返回头节点的下一个结点

		iterator begin(){return iterator(_head->_next);}

这里需要大家注意的是,我们要返回迭代器而不是结点指针,因此我们需要用迭代器的构造函数构造出指向同一块空间的迭代器类型的匿名变量用于返回。 

end是返回最后一个有效数据的后一个数据的迭代器,在双向循环链表中要返回的是头结点

		iterator end(){return iterator(_head);}

当然,除了这两个之外,我们还需要实现两个const版本的函数

		const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}

 六.访问容器相关函数

6.1front和back

front返回第一个有效数据,back返回最后一个有效数据,因此我们在实现front和back函数时,直接返回第一个有效数据的引用和最后一个有效数据的引用即可。

		T& front(){return *begin();}T& back(){return *(--end());}

除此之外,我们还需要重载一对用于const对象的front和back函数。

		const T& front() const{return *begin();//我们已经重载了解引用}const T& back() const{return *(--end());}

6.2插入、删除函数

6.2.1insert

对于insert函数,我们是这样写的:

  1. 首先检查一下插入位置的合法性。
  2. 然后用要插入的数据新建一个结点。
  3. 然后记录下要插入位置处结点的指针
  4. 之后建立节点之间的双向关系
		void insert(iterator pos, const T& x){assert(pos._pnode);node* newnode = new node(x);node* cur = pos._pnode;node* prev = cur->_prev;newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;}
6.2.2erase

erase可以删除所给的迭代器位置的结点。

实现思路为:

  1. 先根据迭代器得到该位置处的结点cur
  2. 然后通过cur找到prev和next指针
  3. 之后删掉cur,并建立prev和next之间的双向关系。
  4. 返回next位置
		iterator erase(iterator pos){assert(pos._pnode);assert(pos != end());node* cur = pos._pnode;node* prev = cur->_prev;node* next = cur->_next;delete cur;prev->_next = next;next->_prev = prev;return iterator(next);}
6.2.3对头尾的插入和删除函数

我们直接复用insert和erase即可。

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

七.其他函数

7.1size

对于size函数,我们可以通过迭代器的遍历获取个数。

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

除了这个方法外,还有一个方法可以获取到个数。

我们可以多设置一个私有成员size,在插入逻辑中,每插入一个则size+1,在删除逻辑中,每删除一个,则size-1。这样也可以获取到size 的个数。

7.2resize

resize函数规则:

  1. 若当前容器的size小于所给n,则尾插结点,直到size等于n
  2. 若当前容器的size大于所给n,则只保留前n个有效数据。

那么,如何实现resize函数呢?

  1. 首先,我们定义一个len表示链表的长度
  2. 遍历到结尾后,比较len和n的大小
  3. 若len较大,则删除掉多余的结点
  4. 若len较小,则尾插数值为x的结点。
		void resize(size_t n, const T& val = T()){iterator it = begin();size_t len = 0;while (it != end()){it++;len++;}while (len < n){push_back(val);len++;}while (len > n){pop_back();len--;}}
7.3clear

对于clear函数,我们需要做的是清空链表的有效数据。

因此我们逐个删除掉链表的结点,只保留头节点即可。

		void clear(){iterator it=begin();while (it != begin()){it = erase(it);//防止迭代器失效}}

7.4empty

empty函数是判空的,我们有很多种方法判断链表是否为空。

这里我们通过判断该容器的begin函数和end函数所返回的迭代器是否相同来进行判断。(如果相同,则代表只有一个头结点)

		bool empty() const{return begin() == end();}
7.5swap

swap函数用于交换两个容器,在list容器当中存储的实际上只有头结点的指针,因此我们只要交换一下头节点的指针即可。

		void swap(list<T>& lt){::swap(_head, lt._head);}

注意点:在此处调用的swap是库中的swap,我们在swap前面加上域作用限定符,即可告诉编译器优先在全局范围内寻找swap函数。 

相关文章:

C++STL详解(五)——list类的具体实现

一.本次所需实现的三个类及其成员函数接口 链表首先要有结点&#xff0c;因此我们需要实现一个结点类。 链表要有管理结点的结构&#xff0c;因此我们要有list类来管理结点。 链表中还要有迭代器&#xff0c;而迭代器的底层其实是指针。但是我们现有的结点类无法完成迭代器的…...

鸿蒙(API 12 Beta3版)【使用投播组件】案例应用

华为视频接入播控中心和投播能力概述** 华为视频在进入影片详情页播放时&#xff0c;支持在控制中心查看当前播放的视频信息&#xff0c;并进行快进、快退、拖动进度、播放暂停、下一集、调节音量等操作&#xff0c;方便用户通过控制中心来操作当前播放的视频。 当用户希望通…...

【STM32项目】在FreeRtos背景下的实战项目的实现过程(一)

个人主页~ 这篇文章是我亲身经历的&#xff0c;在做完一个项目之后总结的经验&#xff0c;虽然我没有将整个项目给放出来&#xff0c;因为这项目确实也是花了米让导师指导的&#xff0c;但是这个过程对于STM32的实战项目开发都是非常好用的&#xff0c;可以说按照这个过程&…...

C#垃圾处理机制相关笔记

C#编程中的垃圾处理机制主要通过垃圾回收器&#xff08;Garbage Collector&#xff0c;GC&#xff09;实现自动内存管理。C#作为一种托管语言&#xff0c;其垃圾处理机制显著减轻了程序员的内存管理负担&#xff0c;与C语言等非托管语言形成鲜明对比。具体介绍如下&#xff1a;…...

C语言memcmp函数

目录 开头1.什么是memcmp函数?2.memcmp函数的内部程序流程图 3.memcmp函数的实际应用比较整型数组比较短整型二维数组比较结构体变量…… 结尾 开头 大家好&#xff0c;我叫这是我58。今天&#xff0c;我们要学一下关于C语言里的memcmp函数的一些知识。 1.什么是memcmp函数?…...

低代码: 组件库测试之Vue环境下的测试工具以及测试环境搭建

Vue Test Utils Vue Test Utils 1 targets Vue 2. Vue Test Utils 2 targets Vue 3. 特别注意要使用 版本 2.0.0 以上 提供特定的方法,在隔离的话环境下,进行组件的挂载,以及一系列的测试 配置开发环境 手动配置, 是比较麻烦的vue cli 是基于插件架构的, 插件可以: 安装对…...

【Vue3】高颜值后台管理模板推荐

ELP - 权限管理系统 基于Vue 3框架与PrimeVue UI组件库技术精心构建的高颜值后台权限管理系统模板。该模板系统已成功实现基于RBAC&#xff08;Role-Based Access Control&#xff09;模型的权限管理系统和字典数据管理模块&#xff0c;后端则使用了Spring Boot框架&#xff0…...

详细介绍Pytorch中torchvision的相关使用

torchvision 是 PyTorch 的一个官方库&#xff0c;主要用于处理计算机视觉任务。提供了许多常用的数据集、模型架构、图像转换等功能&#xff0c;使得计算机视觉任务的开发变得更加高效和便捷。以下是对 torchvision 主要功能的详细介绍&#xff1a; 1. 数据集&#xff08;Dat…...

AI部署——主流模型推理部署框架

我们以最经典的Yolov5目标检测网络为例解释一下10种主流推理部署框架的大概内容&#xff0c;省略模型训练的过程&#xff0c;只讨论模型转换、环境配置、推理部署等步骤。 Intel的OpenVINO — CPUNvidia的TensorRT — GPU/CPUOpenCV DNN Module — GPU/CPUMicrosoft ONNX Runti…...

PyTorch之loading fbgemm.dll异常的解决办法

前言 PyTorch是一个深度学习框架&#xff0c;当我们在本地调试大模型时&#xff0c;可能会选用并安装它&#xff0c;目前已更新至2.4版本。 一、安装必备 1. window 学习或开发阶段&#xff0c;我们通常在window环境下进行&#xff0c;因此需满足以下条件&#xff1a; Windo…...

Vscode——如何实现 Ctrl+鼠标左键 跳转函数内部的方法

一、对于Python代码 安装python插件即可实现 二、对于C/C代码 安装C/C插件即可实现...

力扣热题100_回溯_78_子集

文章目录 题目链接解题思路解题代码 题目链接 78. 子集 给你一个整数数组 nums &#xff0c;数组中的元素 互不相同 。返回该数组所有可能的子集&#xff08;幂集&#xff09;。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 示例 1&#xff1a; 输入&#xff…...

浏览器如何工作(一)进程架构

分享cosine 大佬&#xff0c;版权©️大佬所有 浏览器的核心功能 浏览器&#xff0c;“浏览” 是这个产品的核心&#xff0c;浏览无非分为两步&#xff1a; 获取想浏览的资源 展示得到的资源 现代浏览器还增加了交互功能&#xff0c;这涉及到脚本运行。因此&#xff0c…...

【LeetCode】两数之和

给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案&#xff0c;并且你不能使用两次相同的元素。 你可以按任意顺序返回答案。 示例 1…...

UE5学习笔记11-为拿取武器添加动画

一、一点说明 动画实例通过扩展为所有机器上的每个字符都存在动画蓝图&#xff0c;动画实例只能访问该计算机上的变量。 二、思路 我在武器组件中有一个武器类的指针&#xff0c;判断当前指针是否为空去判断当前角色是否装备武器 三、实现 1.在角色C类中添加是否装备武器的函…...

68. 文本左右对齐【 力扣(LeetCode) 】

一、题目描述 给定一个单词数组 words 和一个长度 maxWidth &#xff0c;重新排版单词&#xff0c;使其成为每行恰好有 maxWidth 个字符&#xff0c;且左右两端对齐的文本。 你应该使用 “贪心算法” 来放置给定的单词&#xff1b;也就是说&#xff0c;尽可能多地往每行中放置单…...

【中等】 猿人学web第一届 第6题 js混淆-回溯

文章目录 请求流程请求参数 加密参数定位r() 方法z() 方法 加密参数还原JJENCOde js代码加密环境检测_n("jsencrypt")12345 计算全部中奖的总金额请求代码注意 请求流程 请求参数 打开 调试工具&#xff0c;查看数据接口 https://match.yuanrenxue.cn/api/match/6 请…...

低、中、高频率段具体在不同应用中的范围是多少

1、低频率段&#xff08;Low Frequency Range&#xff09; ①建筑声学和噪声控制&#xff1a;通常将20 Hz 到 200 Hz 的频率范围视为低频段。在这一范围内&#xff0c;声音的波长较长&#xff0c;通常与低音&#xff08;如重低音音乐&#xff09;和建筑结构中的振动有关。 ②…...

Oxford Model600 Model400低温氦压缩机cryogenic helium compressor手侧

Oxford Model600 Model400低温氦压缩机cryogenic helium compressor手侧...

Golang面试题四(并发编程)

目录 1.Go常见的并发模型 2.哪些方法安全读写共享变量 3.如何排查数据竞争问题 ​4.Go有哪些同步原语 1. Mutex (互斥锁) 2. RWMutex (读写互斥锁) 3. Atomic 3.1.使用场景 3.2.整型操作 3.3.指针操作 3.4.使用示例 4. Channel 使用场景 使用示例 5. sync.WaitGr…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻

在如今就业市场竞争日益激烈的背景下&#xff0c;越来越多的求职者将目光投向了日本及中日双语岗位。但是&#xff0c;一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧&#xff1f;面对生疏的日语交流环境&#xff0c;即便提前恶补了…...

进程地址空间(比特课总结)

一、进程地址空间 1. 环境变量 1 &#xff09;⽤户级环境变量与系统级环境变量 全局属性&#xff1a;环境变量具有全局属性&#xff0c;会被⼦进程继承。例如当bash启动⼦进程时&#xff0c;环 境变量会⾃动传递给⼦进程。 本地变量限制&#xff1a;本地变量只在当前进程(ba…...

Leetcode 3576. Transform Array to All Equal Elements

Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接&#xff1a;3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到&#xf…...

逻辑回归:给不确定性划界的分类大师

想象你是一名医生。面对患者的检查报告&#xff08;肿瘤大小、血液指标&#xff09;&#xff0c;你需要做出一个**决定性判断**&#xff1a;恶性还是良性&#xff1f;这种“非黑即白”的抉择&#xff0c;正是**逻辑回归&#xff08;Logistic Regression&#xff09;** 的战场&a…...

安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件

在选煤厂、化工厂、钢铁厂等过程生产型企业&#xff0c;其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进&#xff0c;需提前预防假检、错检、漏检&#xff0c;推动智慧生产运维系统数据的流动和现场赋能应用。同时&#xff0c;…...

《Playwright:微软的自动化测试工具详解》

Playwright 简介:声明内容来自网络&#xff0c;将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具&#xff0c;支持 Chrome、Firefox、Safari 等主流浏览器&#xff0c;提供多语言 API&#xff08;Python、JavaScript、Java、.NET&#xff09;。它的特点包括&a…...

从零实现STL哈希容器:unordered_map/unordered_set封装详解

本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说&#xff0c;直接开始吧&#xff01; 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...

实现弹窗随键盘上移居中

实现弹窗随键盘上移的核心思路 在Android中&#xff0c;可以通过监听键盘的显示和隐藏事件&#xff0c;动态调整弹窗的位置。关键点在于获取键盘高度&#xff0c;并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合

在汽车智能化的汹涌浪潮中&#xff0c;车辆不再仅仅是传统的交通工具&#xff0c;而是逐步演变为高度智能的移动终端。这一转变的核心支撑&#xff0c;来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒&#xff08;T-Box&#xff09;方案&#xff1a;NXP S32K146 与…...

QT3D学习笔记——圆台、圆锥

类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体&#xff08;对象或容器&#xff09;QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质&#xff08;定义颜色、反光等&#xff09;QFirstPersonC…...