【C++】STL | vector 详解及重要函数的实现

目录

前言

总代码

vector类框架建立(模板与成员变量)

构造、析构、swap 与 赋值重载

构造

析构

swap

赋值重载

reserve 扩容(重要!!)、size、capacity

operator[ ]重载

insert 插入

逻辑讲解

insert中的迭代器失效

erase 删除

删除主逻辑

迭代器失效

push_back、pop_back

begin 与 end

拷贝构造、n个值构造

一段迭代器区间构造

initializer_list

结语


前言

vector在STL中属于较为简单的知识点,学习难度相对较低,但是相比于string,本篇博客的底层实现会添加一些很有意思且很实用的东西,比如:initializer_list、一段迭代器区间初始化等等

如果有对库中的vector有了解需求的话,可以看一下C++中较为官方的网站,这里面有详细的讲解和用法介绍,网址如下:

https://legacy.cplusplus.com/reference/vector/vector/?kw=vector

总代码

如果有友友只是复习需要,只想看完整代码的话,可以直接点下面的gitee链接

当然对于看完了整篇文章的友友也可以照着这里的代码敲一篇进一步理解喔...(* ̄0 ̄)ノ

gitee - vector - blog - 2024-08-06

vector类框架建立(模板与成员变量)

首先我们可以将我们的类包在一个命名空间里面这样就不会在写完之后因为名字与库中的相同而报错

接着,我们可以写一个类模板,我们vector的本质就是数组,而我们的数组里面可以存任何类型,可以存一个int,可以存char,甚至可以存一个vector<int>类型实现二维数组

所以我们需要写一个模板T,表示类型

接着就是成员变量,这个地方有多种选择,我们可以和上一篇的string一样使用一个指针,一个size一个capacity的策略

但由于源码中的成员是三个迭代器,一个指向开始位置,一个指向有效数据的尾(有效数据个数),一个指向整个空间的尾(总的空间大小),虽然都可以,但我们还是尽量与源码保持一致

由于条件的需要,我们先来谈谈vector中的迭代器是什么:

由于我们的vector是一块连续开辟的空间,所以我们的迭代器和string的一样,都是原生指针,并不需要单独写一个类来实现迭代器的功能,如下:

//迭代器
typedef T* iterator;
typedef const T* const_iterator;

成员如下:

namespace hjx
{template<class T>class vector{// 此处的迭代器就是T*private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;};
}

构造、析构、swap 与 赋值重载

构造

由于我们的三个成员本质上都是iterator,也就是原生指针,所以我们并不需要专门写一个构造函数来初始化这三个指针

至于一段迭代器区间初始化,initializer_list初始化,n个值的初始化,拷贝构造

这些需要我们后面讲完了插入之后才能进行讲解,因为其主逻辑都是一个一个插入

这里既然不需要显示写,但又需要生成,不然就没法写拷贝构造

所以我们可以使用下面的写法:

//编译器强制生成构造函数
vector() = default;

析构

这里的析构主要就干两件事:

  1. 将空间销毁
  2. 将指针全部置为空指针

当然这一切都得在空间存在的情况下才需要执行,所以我们还可以添加一个判断条件

代码如下:

//析构
~vector()
{if (_start){delete[] _start;_start = _finish = _end_of_storage = nullptr;}
}

swap

由于vector的交换函数只需要交换三个指针,库中的swap默认会直接交换空间,所以我们就需要自己显示写

但是主要的逻辑还是借助库中的swap函数将指针一个一个交换

代码如下:

void swap(vector<T>& v)
{std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_end_of_storage, v._end_of_storage);
}

赋值重载

这里我们可以使用”现代写法“:

我们在接收参数的时候,如果不加引用,那么默认接收的是这个参数的拷贝

既然如此,我们vector的拷贝也有空间,我们就只需要交换一下两边的指针,将参数的空间交换过来,销毁的时候调用不到这块空间,就不会销毁

有个词很符合这中做法:”剥削“(bushi

代码如下:

//赋值重载
vector<T>& operator=(vector<T> v)
{swap(v);return *this;
}

reserve 扩容(重要!!)、size、capacity

在该函数中,我们的参数是一个 size_t 的类型,意味要将空间扩容为多大,也就是将扩容后的总空间大小传过来了

而我们实现扩容时,有一个需要注意的点,就是迭代器失效

因为我们C++的扩容需要自己开空间,拷贝数据,销毁久空间,所以有可能出现原来的指针还指向已销毁空间但我们还需要用的情况,这是迭代器失效的其中一种情况

而我们后面要重新另指针指向新空间时,需要根据每个指针之间的距离作为考量来初始化,如下:

_start = tmp;
_finish = _start + len1;
_end_of_storage = _start + len2;

但如果 len1   len2 就是通过指针本身计算的呢?

假设 len1 = _finish - _start,但是此时_finish 还指向旧空间,这么计算就错了

所以我们需要提前存储一份oldsize

而扩容的主逻辑无非就是

  1. 开新空间
  2. 拷贝数据
  3. 销毁旧空间

但是第二第三步是要在旧空间存在的情况下才执行的,如果旧空间本身就为空,那么我们自然不需要拷贝数据与销毁

代码如下:

void reserve(size_t n)
{如果原空间不够了才需要扩容if (n > capacity()){//提前记录,后面会出现迭代器失效的情况size_t oldsize = size();T* tmp = new T[n];//如果旧空间本身就为空,则不需要拷贝数据与销毁if (_start){for (size_t i = 0; i < oldsize; i++){tmp[i] = _start[i];}delete[]_start;}_start = tmp;_finish = _start + oldsize;_end_of_storage = _start + n;}
}size_t capacity()const
{return _end_of_storage - _start;
}size_t size()const
{return _finish - _start;
}

如上我们还添加了size和capacity函数的实现,两个的主逻辑就是指针的相减

operator[ ]重载

我们执行operator[ ]时想要的效果就是返回那个位置的值,所以我们返回就是了

代码如下:

//opertaor[]
T& operator[](size_t i)
{return _start[i];
}const T& operator[](size_t i)const
{return _start[i];
}

insert 插入

逻辑讲解

我们能看到,库中的第一个参数都是迭代器类型的,我们自己实现的版本也需要为位置为迭代器的类型(假设该参数名为pos

至于第二个参数自然是传入的要插入的参数了

插入的大逻辑是:

  1. 先判断是否需要扩容,如果需要就将扩容后的总大小作为参数传给reserve
  2. 将pos位置后的所有数据全部往后移一位,将数据插入
  3. 处理成员指针

前两点和我们上一篇讲解string的处理方法极为相似,代码如下:

//判断扩容
if (_finish == _end_of_storage)
{size_t len = pos - _start;size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();reserve(newcapacity);pos = _start + len;
}
//插入数据
iterator end = _finish;
while (end > pos)
{*end = *(end - 1);--end;
}

扩容时需要注意的点就是:扩完容之后我们的空间就是新空间了,但是我们的pos还指向原来的空间,所以我们需要提前将数据存储一下,扩容完成后再更新pos

而我们的所有数据向后移,直接使用一个while循环即可,就像我们使用迭代器遍历的那样

insert中的迭代器失效

由于我们的插入需要扩容,如果我们在外面使用迭代器访问的话,就有可能出现迭代器还指向原空间的情况,进而导致随机值的出现,如下:

auto it = v1.begin();
v1.insert(v1.begin()+2, 1000);

如果我们的insert此时插入之后刚好扩容了,那么我们的迭代器 it 还指向原空间,如果再去使用的话,就是越界访问了,也就是迭代器失效的一种情况

为了解决这种情况,C++源码中的解决方案是:将insert后的地址返回,然后我们在外面使用的时候再进一步的赋值即可,如下:

auto it = v1.begin();
it = v1.insert(v1.begin()+2, 1000);

综上,我们的insert需要一个iterator类型的返回值

这才是参数需要iterator类型的意义

总代码如下:

//insert
iterator insert(iterator pos, const T& x)
{assert(pos <= _finish);assert(pos >= _start);//判断扩容if (_finish == _end_of_storage){size_t len = pos - _start;size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();reserve(newcapacity);pos = _start + len;}//插入数据iterator end = _finish;while (end > pos){*end = *(end - 1);--end;}//插入数据*pos = x;_finish++;return pos;
}

erase 删除

删除主逻辑

删除其实就只干两件事:

  1. 移动数据向前覆盖一格
  2. --_finish(_finish指向的是有效数据的尾部,减减之后就不会访问到最后一个位置,后面要再插入也会将其覆盖)

到这里,大体上和insert也差不多,接下来我们就来详细讲讲erase的迭代器失效

迭代器失效

我们来看,如果我们要删除的是最后一个位置,那么删除了之后,指针(也就是迭代器)没有更新,就会造成越界访问

另外在有一些平台下面,当数据减少到一个程度之后,会执行缩容操作,此时我们的迭代器也会指向错误的位置造成越界访问

所以这么看,erase也是会造成迭代器失效的,而处理的方法和insert一样,将删除后位置的迭代器返回即可

综上,代码如下:

//erase
iterator erase(iterator pos)
{assert(pos < _finish);assert(pos >= _start);iterator it = pos + 1;while (it != _finish){*(it - 1) = *it;it++;}--_finish;return pos;
}

push_back、pop_back

上文中我们已经实现了insert了,所以push_back也可以复用insert,因为其本质就是在尾部插入一个数据

代码如下:

void push_back(const T& x)
{insert(end(), x);
}

而我们的pop_back相对简单,倒也不需要复用erase,只需要将Z_finish--即可,还是相当简单的

代码如下:

void pop_back()
{assert(size() > 0);--_finish;
}

begin 与 end

//迭代器
typedef T* iterator;
typedef const T* const_iterator;iterator begin()
{return _start;
}iterator end()
{return _finish;
}const_iterator begin()const
{return _start;
}const_iterator end()const
{return _finish;
}

拷贝构造、n个值构造

我们的拷贝构造本质上也是构造一个类,只不过是拷贝了另一个vector而已

而无论是什么写法,即使是现代写法在参数那里编译器也会复制一遍,其他写法也是一个一个放数据,本质上效率都一样

所以我们这里的写法就是:直接使用范围for遍历另一个vector,再将值一个一个push_back即可

但是在此之前,我们还需要先reserve出一块空间,虽然说push_back内部也有扩容逻辑,但是我们提前开好倒也算是减去了一部分消耗

代码如下:

//拷贝构造
vector(vector<T>& v)
{reserve(v.capacity());for (auto e : v){push_back(e);}
}

而n个值初始化也是同理,我们只需要写一个for循环循环n次,再执行push_back的逻辑即可

代码如下:

vector(size_t n, const T& val = T())
{reserve(n);for (size_t i = 0; i < n; i++){push_back(val);}
}vector(int n, const T& val = T())
{reserve(n);for (size_t i = 0; i < n; i++){push_back(val);}
}

这里之所以写两个版本(一个size_t, 一个int),是因为后面我们在实现一段迭代器区间构造的时候会用到函数模板,如果我们n个值构造传的是一个int,一个其他类型的倒还好

但如果我们传两个int的话会匹配到迭代器区间初始化那里去

所以我们在写size_t版本的同时再写一个int的版本,为的就是应对这种情况

一段迭代器区间构造

//一段迭代器区间初始化
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);++first;}
}

我们之所以要搞一个模板,为的就是无论什么结构的数据,都能初始化到vector这来,如果我们单纯给T的话,就只有vector的迭代器区间可以放进来初始化

但如果我们写了模板,那么无论是链表的,栈的,红黑树的等等,都可以放数据进来给vector初始化

而内部逻辑也与上述的拷贝构造、n个值构造一样,用的主要都是push_back的逻辑,这里就不过多讲解了(因为就是将迭代器指向的值一个一个尾插进来,再将迭代器往下移动一个而已)

initializer_list

我们通过查看文档可以发现,initializer_list本质上就是一个类,类里面有几个函数,仅此而已

所以我们可以直接将initializer_list中的值拿来push_back,由于里面有begin和end函数,这也就意味着我们可以使用范围for对initializer_list进行遍历(〃 ̄︶ ̄)人( ̄︶ ̄〃)

代码如下:

vector(initializer_list<T> il)
{reserve(il.size());for (auto e : il){push_back(e);}
}

结语

本篇文章关于vector的讲解到这里就结束啦(~ ̄▽ ̄)~

如果觉得对你有帮助的话,希望能多多关注喔(○` 3′○)

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

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

相关文章

Oracle认证1Z0-071线上考试注意事项

目录 一、前言二、回顾过往战绩第一次 裸考&#x1f412;第二次 背题库硬考&#xff01;&#x1f412;第三次 软件卡住&#xff0c;寄&#xff01;&#x1f648;第四次 汇总纠错&#xff0c;通过&#xff01;&#x1f31a; 三、考试流程四、考试注意事项1. 是否需要科学上网2. …

探索四川财谷通抖音小店:安全与信赖的购物新体验

在数字经济蓬勃发展的今天&#xff0c;抖音平台凭借其庞大的用户基础和强大的内容生态&#xff0c;逐渐成为了电商领域的一股不可忽视的力量。其中&#xff0c;四川财谷通抖音小店作为这一浪潮中的佼佼者&#xff0c;不仅以其丰富的商品种类和独特的品牌魅力吸引了众多消费者的…

Java多线程的单例设计模式 多种实现方法

目录 前言 饿汉式 懒汉式 Double_check volatile double_check Holder方式 枚举 前言 单例设计模式GOF23中设计模式中最常用的设计模式之一, 单例设计模式提供了多线程环境下的保证唯一实例性的解决方案, 虽然简单, 但是实现单例模式的方式多种多样, 因此需要从多个维度去…

[安洵杯 2019]easy_serialize_php

[安洵杯 2019]easy_serialize_php [安洵杯 2019]easy_serialize_php - DGhh - 博客园 (cnblogs.com) [安洵杯 2019]easy_serialize_php - 何止(h3zh1) - 博客园 (cnblogs.com) 涉及的考点是字符串逃逸 <?php //GET一个f $function $_GET[f];//定义过滤的字符串数组 fu…

c++初阶-------模板

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

科普文:微服务之全文检索SpringBoot整合ElasticSearch说明

一、RestHighLevelClient介绍 JavaREST客户端有两种模式&#xff1a; Java Low Level REST Client&#xff1a;ES官方的低级客户端。低级别的客户端通过http与Elasticsearch集群通信。Java High Level REST Client&#xff1a;ES官方的高级客户端。基于上面的低级客户端&…

Io 35

FIleinputStream字节输入 package File.io;import java.io.*;public class io1 {public static void main(String[] args) throws IOException {// InputStream is new FileInputStream(new File("C:\\Users\\SUI\\Desktop\\Java1\\one\\src\\kaishi"));//简化Input…

C++ 几何算法 - 求两条直线交点

一&#xff1a;算法介绍 1. 首先定义两条直线方程&#xff1a; 2. 解方程&#xff0c;求出x, y坐标 3. 如果x分母的行列式等于0&#xff0c; 说明两条直线平行或方向相反 4. 如果x&#xff0c;y分母的行列式都等于0&#xff0c;说明两条线重叠 二&#xff1a;代码实现: #inclu…

求职Leetcode题目(5)

1.分割回文串 每一个结点表示剩余没有扫描到的字符串&#xff0c;产生分支是截取了剩余字符串的前缀&#xff1b;产生前缀字符串的时候&#xff0c;判断前缀字符串是否是回文。如果前缀字符串是回文&#xff0c;则可以产生分支和结点&#xff1b;如果前缀字符串不是回文&#…

Vue常见问题(一)组件的使用

Failed to resolve component. 报错原因&#xff1a; 组件注册错误&#xff1a;我们在组件中使用了未注册的组件。在Vue中&#xff0c;组件必须先注册才能使用。 解决方法&#xff1a; 引用组件 &#xff1a; import ItemPage from "/components/itemPage.vue";…

【踩坑】pytorch中的索引与copy_结合不会复制数据及其解决方案

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 目录 背景知识 实验验证 结论分析 错误案例 处理方法 注意事项 附加说明 基本索引返回视图 高级索引返回副本 赋值操作都是原地操作 以下内容…

重生之我 学习【数据结构之顺序表(SeqList)】

⭐⭐⭐ 新老博友们&#xff0c;感谢各位的阅读观看 期末考试&假期调整暂时的停更了两个多月 没有写博客为大家分享优质内容 还容各位博友多多的理解 美丽的八月重生之我归来 继续为大家分享内容 你我共同加油 一起努力 ⭐⭐⭐ 数据结构将以顺序表、链表、栈区、队列、二叉树…

索尼相机SD卡找不到视频怎么办?提供全面解决方案

在使用索尼相机拍摄美好瞬间时&#xff0c;SD卡作为存储介质&#xff0c;承载着珍贵的视频和照片。然而&#xff0c;有时我们可能会遇到SD卡中视频文件无法找到的问题&#xff0c;这无疑让人倍感焦虑。本文旨在为大家提供一套全面的解决方案&#xff0c;希望帮助大家快速找回丢…

探索Linux世界之Linux环境开发工具的使用

一、yum -- Linux软件包管理器 1、什么是yum yum(Yellow dog Updater, Modified)&#xff1a; 是Linux下非常常用的一种包管理器. 主要应用在Fedora, RedHat, Centos等发行版上。 在Linux上安装软件的方式&#xff1a; 源代码直接安装&#xff1a;在Linux下安装软件, 一个通…

The Llama 3 Herd of Models 第8部分语音实验部分全文

第1,2,3部分,介绍、概览、预训练 第4部分,后训练 第5部分,结果 第6部分,推理 第7部分,视觉实验 8 Speech Experiments 我们进行了实验来研究将语音功能集成到Llama 3中的组合方法,类似于我们用于视觉识别的方法。在输入端,一个编码器,连同一个适配器,被并入处理语…

uniapp vue3 转换华为鸿蒙(以及问题一些解决方案)

主要是从 Windows系统配置 、配置离线SDK和DevEco-Studio、HBuilderX、三方面进行配置。 因为我也是之前写小程序的用uniapp vue3 写的看官网&#xff08;uni-app 开发鸿蒙应用 | uni-app官网&#xff09;的时候看到vue3 uniapp 写法可以转换华为鸿蒙开发&#xff0c;我就自己来…

为什么要用分布式锁

单应用中,如果要确保多线程修改同一个资源的安全性 加synchronized就可以了 但是性能不高 而mybatis-plus的乐观锁就可以很好的解决这类问题 但是这样的锁机制,只在单应用中有效 试想,在分布式下,有没有可能出现多个应用中的线程同时去修改同一个数据资源的并发问题 例如A …

Rstudio Server常见问题处理手册

一.开头 上面这个界面是不是非常熟悉&#xff1f;Rstudio 死亡圈圈一般发生在输入账号密码后进入Rstudio的时候&#xff0c;如果之前运行过大任务&#xff0c;有可能会出现这种情况。Rstudio常见问题我们如何排查和处理,本文章将给你一些思路和处理方式。 【ads】如果您不想被…

【开源】嵌入式Linux(IMX6U)应用层综合项目(4)--音乐播放器APP

1.简介 此文章并不是教程&#xff0c;只能当作笔者的学习分享&#xff0c;只会做一些简单的介绍&#xff0c;其他的各位结合着代码和运行现象自己分析吧&#xff0c;相信通过函数名和注释&#xff0c;基本上是不难看懂代码的&#xff0c;其中涉及到的一些技术栈&#xff0c;也…

图论(强联通分量)

在图论中&#xff0c;特别是在讨论有向图&#xff08;Directed Graph&#xff09;时&#xff0c;我们常常需要了解图的结构特性&#xff0c;比如强联通分量&#xff08;Strongly Connected Components, SCC&#xff09;。了解强联通分量中的各种边对于理解图的整体结构以及某些…