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

C++笔记-vector

一.vector的基本使用

1.1vector的基本概念

在 C++ 里,std::vector是标准模板库(STL)提供的一个动态数组容器。它能存储同一类型的多个元素,而且可以在运行时动态改变大小。

其实vector就是一个顺序表,而顺序表我们之前都学习过,所以vector和string底层都为数组,所以两者在库中的一些接口等都非常相似:

可以看出vector的接口很多都是我在string类中讲过的,并且因为两者底层都为数组,所以在使用上也是基本一样的。

1.2vector的基本使用

因为和string高度相似,所以我就演示一下vector的基本用法,主要是将下面的实现vector,来帮助我们更好地理解vector。

尾插和遍历数组都和string是一样的,唯一的区别是我们在创建vector对象时要加上<>,里面标明数组中的数据是什么类型。

迭代器和范围for来遍历数组也是和string一样的方式。

二.vector的实现

2.1vector框架的实现

和之前的string一样,首先把vector的框架写出来。

和之前string不一样的是,这里vector我们要用到模板,因为底层vector就是用模板那来实现的。

另外成员变量也能也有人不理解,这里这样定义成员变量的名字和成员变量的类型是因为vector底层源代码是这样定义的,源代码大家有机会可以看看,我们既然实现vector,就尽量跟着底层走。

我们知道vector本质是一个顺序表,所以_start代表的就是首元素地址,_finsh代表的就是最后一个元素的下一个位置,_end_of_storage代表的就是容量。

因为它们都是指针,所以构造和析构实现时都把它们置为nullptr,另外把begin和end这两个迭代器实现了,后面也会使用。

注意:这次vector中各种接口的声明和实现都要在类内实现,因为我们使用了模板,模板如果分开声明和实现会出现链接错误。

2.2cpapcity的实现

这里直接用_end_of_storage - _start就可以直接得到此时数组容量的大小,指针相减得到就是两个之间的大小。

2.3size的实现

同样的方式,用_finsh - _start就可以得到数组中数据的数量。

2.4reserve的实现

在实现reserve中,如果此时_start为空,就可以直接让_start接收tmp的位置。

这里面最大的问题就是计算_finsh的位置,如果不提前计算此时的size,那么后面扩容后,_start已经转移到新的地址,而此时我们调用size来计算_finsh时,就会出问题。

因为此时_start在新的空间,而_finsh在旧的空间,如何计算size?

直接相减肯定会出问题,所以要提前计算size的大小,就是为了避免这种问题。

剩下的就是通过_start来找到_finsh和_end_of_storage的位置即可。

2.5push_back的实现

逻辑和之前实现string中的push_back一样,在尾插前检查此时数组是否满了,满了就扩容,最后在_finsh处加上目标值,并令_finsh++即可。

2.6pop_back的实现

这个实现起来就很简单,不需要将最后一个值置为0,因为如果走后一个值本来就是0呢?

所以_finsh--即可。

2.7[]符号的实现

实现起来也比较容易,直接返回相应下标的值即可。

2.8insert的实现

insert大体逻辑和之前string中之一样的,不一样的是在string中我们用下标来遍历数组,但是会出现头插时类型提升的问题,这次我们使用指针来实现,可以解决之前的问题。

但用指针又出现了新的问题,就是图片中所说的迭代器失效的问题,解决问题的关键就是pos,上面的代码是解决问题后的代码。

我们接下来看看如果不做处理,迭代器失效会出什么问题:

注意:要检查本身insert中代码实现是没问题的。

此时就出现问题了,insert本身的代码是没问题的,我们再看另一个现象:

此时又没问题了,我们通过刨析代码发现,出现这两种情况是因为第一个扩容了,而第二个扩容了,第二个例子没问题说明reserve本身是没问题的,那么就是因为扩容才导致了这种问题。

其实这个问题和上面reserve遇到的问题很相似:

用图来解释就是我们扩容后,_start已经来到了新的空间,而pos依旧指向旧空间,那么在通过it和pos位置遍历数组是不是就会出问题?

这就是迭代器失效,在这里本质上就是野指针的问题。

而为什么第二个例子没有问题的呢?

因为第二个例子在尾插前就已经扩容过了,此时空间没有满,用的还是原来的空间,所以不会出问题,所以为了解决第一个例子出现的迭代器失效的问题,我们在扩容前就记录下pos和_start之间的距离,扩容后更新pos的位置,这样就能解决问题。

我们再来看一个问题:那尾插后我要访问pos位置处的值呢?


已经解决了扩容后导致的问题,为什么又不能访问pos的值了呢?明明已经更新了pos的位置。

因为我们实现的是传值调用,如果用引用就可以解决这个问题,但是此时又会出现新的问题:

我们在这种使用时就会出问题,因为此时的pos是临时对象,而临时对象具有常性,也就是之前讲的权限扩大的问题,可能又会与人说加个const就行了,那又会出现新的问题,这里我就不过多赘述了,而底层用的也是传值调用,所以就按这个来。

所以说呢insert之后就默认pos已经失效了,不能再使用pos了,因为并不清楚底层什么时候会扩容。

2.9构造函数的其他实现方式

这两种方式都是对数组进行初始化,第一种方式是初始化为n个一样的值,第二种方式是直接初始化为任意个一样或不一样的值。

第一种方式中的参数利用了匿名对象,因为要给缺省值,但是不能直接给0,因为数组中不一定就是整型,所以利用匿名对象就可以解决这个问题。

第二种方式的参数有的人会看不懂,这种方式其实c++11中引入的一种方式,它是std标准库中的,它里面自带begin,end和size。

两种构造函数的基本用法就如上图所示。

2.10erase的实现

这是正常情况下我们要实现的erase函数,但是这种方式会出问题:

这里我们先用系统给的vector来实现这段代码,发现出现了错误,但是我们检查这段代码的逻辑是没有问题的,会出现这个错误的原因就是和上面一样,都是迭代器失效的问题。

因为在vs编译器下,如果调用了erase函数,会默认当前的迭代器已经失效,然后会强制检查迭代器,所以我们对vv++就会出问题。

而在其它编译器下不一定会有这种问题,就比如g++编译器下,就不会强制检查,所以相同的代码下就不会报错。

所以我们要怎样解决这种问题呢?

其实只需要更新迭代器的位置即可,使它是有效的,具体过程如下:

将erase函数返回的位置赋值给vv即可,其实vv本身的位置没有改变,只不过将它原本的值重新赋值给自己而已,使其重新变为有效的迭代器,所以我们上面写的erase函数就不太合适,应该写成如下这样:


其实去看vector底层实现的erase接口,也是这样实现的,是有返回值的,不是我们之前实现的void。

2.11resize的实现

vector中resize的实现比较string就会更容易些,就分为两种情况,大于capacity和小于等于capacity,大于capacity是扩容+插入数据即可,小于等于时,更新_finsh的位置即可。

即使感觉如果n>size && n<capacity这个范围怎么办,其实缺省值就把这个问题解决了,超出的部分都初始化为0。

2.12=符号的实现

=符号的实现需要用到swap函数,这样实现起来其更为简单。

swap:我们之前的传统逻辑是再创建一块空间,把其中一个数据先拷贝到临时空间,再进行交换。而现在使用的现代写法直接交换两个指针,不需要再创建新的空间。

可能有人会疑惑为什么还要交换_finsh和_end_of_storage,如果不交换这两者的话,那么交换后的_start中的_finsh和_end_of_storage是不对应的,这么做就会使两者的意义就没有了,所以两者也要交换。

注意:要使用的是std标准库中的swap函数,std库中的swap函数是一个模板,string和vector中的swap接口都是基于std标准库中的swap函数来实现的。

完成swap函数再实现=符号就简单许多,直接交换即可,最后返回*this。

注意:这里不能使用引用,因为我们实现的是=符号,不能改变等号右边的值,所以要使用传值调用。

最后再来看一种现象:

我们不再使用int类型,使用string类型,此时我们在vector插入几段字符串没有问题把,我们再看:

此时就会出问题了,但问题出在哪儿了呢?

很明显,问题出在扩容这步操作了,但是扩容我们之前都已经试验过了,没有问题,我们来调试看看:

我们发现在经过delete这段代码后,就出现这种现象了,那说明delete这部操作有问题,但是_start指向的就是一个数组,这样delete没有问题啊。

是的,怎么看这段代码都没有问题,那么是哪里出的问题呢?

这里我直接就说答案了,是memcpy出的问题导致的这种现象,我们通过画图来演绎出memcpy的过程:

把图画出来后相信大家一眼就看出问题出在哪儿了,这就是使用memcpy的弊端,当vector中是内置类型时,这么写是没有问题的,但是对于自定义类型这么些就不行了。

所以要怎样解决这种问题呢?,很简单:

直接利用循环一个个复制过去即可,有人会疑惑为什么这样就可以解决了呢?
拿string举例,当赋值的时候,就会调取string中的拷贝构造函数,而string底层中的拷贝构造函数是深拷贝,所以就解决了这种问题,内置类型就更可以实现了。

以上就是vector的内容。

相关文章:

  • 高光谱相机:温室盆栽高通量植物表型光谱成像研究
  • 安全编码课程 实验7 并发
  • 如何用服务预约让客单价提升20%?
  • 图像预处理-边缘填充,透视变换和色彩空间基础
  • go中我遇到的问题总结
  • 7.5 使用MobileNet v3进行图像的区分
  • 阿里滑块 231 231纯算 水果滑块 拼图 1688滑块 某宝 大麦滑块 阿里231 验证码
  • 【CHNS】 各个文件,数据信息备注
  • C++ (new和delete运算符,链接库,面向对象与面向过程)
  • SecProxy - 自动化安全协同平台
  • [reinforcement learning] 是什么 | 应用场景 | Andrew Barto and Richard Sutton
  • ros2_01
  • Qt QML - qmldir使用方法详解
  • Leetcode——137 260找出只出现一次的数
  • 【密码学——基础理论与应用】李子臣编著 第六章 祖冲之序列密码 课后习题
  • Trae 下安装 Pylance 插件(仅作为实验,版权由微软所有)
  • 【lerobot】3-开源SO-100 主从臂的舵机位置校正、遥控操作(ubuntu系统)
  • 基于图扩散小波的连接组分析:定位结构-功能映射中的扩散源
  • docker部署GPUStack【Nvidia版本】
  • 【Hot100】239. 滑动窗口最大值
  • 儿童阅读空间、残疾人友好书店……上海黄浦如何打造城市书房
  • 不断深化“数字上海”建设!上海市数据发展管理工作领导小组会议举行
  • 江苏东海县多个商家直播带货玉石珠宝以假充真、虚假宣传被整治
  • 企业跨境支付的最大挑战及解决方案
  • 打捞一条文学传统的暗线
  • 阮刚辉已任长春市副市长,去年由浙江跨省调任吉林