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

【C++】简单学——vector类(模拟实现)

模拟实现的准备工作

看源码,了解这个类的大概组成

1.先看成员变量

成员变量的组成是三个迭代器

问:这个iterator内嵌类型究竟是什么?即这个迭代器是什么 

迭代器实际就是T*

问:这三个迭代器代表什么意思?

连蒙带猜:元素的开始,元素的结束,容量的结束

不过因为只是猜测,所以还是需要去自己试验一下的

2.看构造函数

要看对象要怎么初始化

无参的时候是全部迭代器初始化成空 

3.看pushback(增加数据)

如果经验丰富的话,就可以知道这个是尾插

问:这个construct是什么意思?

内部其实就是一个定位new

定位new是对已有的空间进行初始化

construct是在finish的位置那里进行尾插,然后finish++

STL的空间都是从空间配置器(内存池)来的,内存池只负责开辟空间,没有初始化,如果说要对已有的空间进行初始化,就要用定位new

问:insert_aux(end(),x)又是什么意思?

如果说finish==end_of_storage,就扩容,可以从这里看出来,vs的扩容是2倍扩容

总结

由上面的信息可以得出下面的图和信息

size就是finish-start

capacity就是end_of_storage-start

开始尝试能不能实现

因为我们是模拟实现,所以会有很多没有考虑到的地方,所以如果我们要写一些方法,强烈推荐写一个画一个图,这样还可以来查漏补缺,看看那些隐藏的bug

模拟实现的意义

1、让我们更加理解底层,能够更好地使用

2、可以跟那些大佬学习他们是怎么实现的(尽管因为大佬在搞的时候会因为考虑全局方面而被迫搞得很复杂,但是仍然有参考意义)

尝试把刚刚前面了解的东西写一下

问:既然我们都已经写了缺省参数,并且我们也没有必要搞有参构造函数,那能不能把那个无参拷贝构造给删掉

答:如果说之后写了拷贝构造,编译器就不会生成默认构造函数,就会导致当你不传参构造对象的时候没有默认构造函数可以调用

析构函数

为了方便测试,所以先写一个pushback尝试跑通一下

对于类模板,函数如果要用到模板,就要特别小心了

除了考虑各种值,还要考虑各种值是否合适

例如:假如这个地方push _back的是一个string或者是vector等等,就会导致消耗很大;如果加了引用,还要小心这个传入的值在里面被修改了

(解决办法:加引用,加const)

push_back的逻辑:

1.先判断够不够容量

如果要扩容,就记录一下新开的空间要多大,至于size和capacity怎么算,我们就顺便把size和capacity给实现了吧

扩容逻辑:根据原本的容量进行二倍扩容,然后把原有的数据拷贝下来,然后原本的start指向的空间就销毁,然后让start指向新的空间,把那些新的成员变量都更新一遍

插入逻辑:在finish的位置的值设置为val,然后finish++往后移一格

问:如果说vector实现了析构函数,tmp开辟了空间,tmp的生命周期结束的时候难道不会把开辟的那个空间给delete掉吗?

答:我们必须得区分好,只有对象才会在销毁的时候自动调用析构函数,对象是包括了_start、_finish、_endofstorage的对象,而我们现在的开辟空间只不过是利用一个T的指针去在堆里面开辟空间,并不是对象,不会自动销毁堆的空间

为了方便测试,我们就做一个遍历吧,为了遍历方便,顺便做一个operator[ ]

(记得包assert的头文件)

测试用例:

但是结果出问题了

调试之后,发现finish在进行更新的时候出了问题

问:为什么这个地方的_finish变成了空指针

直到_start更新都是没问题的,但是这个地方的finish = tmp + size();而size是等于finish - start;

也就是说,此时的start已经更新了,而finish还没有更新,所以size = (旧的)finish - (新的)start

所以公式就变成了这样:(新的)finish = (新的)start + (旧的)finish - (新的)start = (旧的)finish

因为旧空间在前面就已经被销毁了,所以新的finish仍然指向这个被销毁的空间,所以_finish就变成了空指针

解决办法:提前记录size(记录start和finish的相对位置)

 size和capacity

迭代器

测试用例:对比三种

const的也要实现

晚点把size和capacity也实现const

为了方便我们进行测试,我们直接把刚刚遍历的内容搞成一个print函数来调用吧

当然,也可以实现成模板

但是这里有一个潜藏的问题:因为这个print用了auto所以才避免了

报错的原因:

语法上有冲突,编译器不认识这个const_iterator是什么东西

因为这个函数是函数模板,所以里面的一切模板都是没有实例化的(实例化是在后面调用的时候)

编译器是不敢去没有实例化的模板里面去取东西的

编译器会根据这种(通过指定类域的方式来取数据)情况有两种猜测:

  1. 这是一个内嵌类型(typedef定义的类型,内部类等等在类里面定义的类型)
  2. 这是一个类里面的静态变量,如果说是静态变量,那后面肯定就是要去跟分号( ;)的了

为了解决这个问题,编译器规定这种情况要在前面加上typename

pop_back和empty

尾删直接让finish往前移动一格就好,为了防止一直往后删,所以要写一个判断为空的方法

insert

我们就实现第一个就好

插入之前先检查空间够不够(顺便把reserve给实现了)

然后把数据插入到pos位置

把前一个数据挪到后一个数据处,直到把pos位置的数据给挪走

问:string的时候,pos是size_t,导致头插的时候,判断条件让size_t it 永远不会比pos小而死循环,为什么现在不会有这个问题了?

答:我们现在的pos是迭代器,而地址永远都不会为0(地址本质上是每个内存单位的编号,最小的指针就是空指针),所以不存在string的那种问题

出问题了

这个地方我们选择不断头插11.11

当超过4个值,要进行扩容的时候就出错了

调试后发现,pos的位置不对劲,我们原本是打算pos指向头元素的,但是扩容之后,原来的那个空间被销毁了,但是pos还是在旧空间上(迭代器失效)

问:为什么会出现-6.2777等的数字?

答:因为pos没有更新,所以当进行it>=pos的时候,此时的pos是0x0163f068,it是0x01635250,此时的pos要比it大,所以就不会进到循环判断中,也就是啥都没做,但是finish却凭空++了,然后就会看到尾部多了一个非法的数据;(这种情况是因为迭代器失效导致的,所以正常情况下不扩容是看不到这个错误的)

解决办法:提前记录pos和start的相对位置

reserve

直接把前面push_ back时临时搞的扩容直接拷贝过来就好

最终实现

push别忘了改回去

erase

依次把pos+1的位置往前覆盖,直到pos+1到了结尾

由此可以复用pop_back

resize

根据库里面实现的resize,我们可以得到以下的函数

const T& val = T()是一个调用了默认构造的无参的匿名对象

好处:可以根据不同的类型来给初始值(因为string和其他更多类型不能简单的用0来初始化,就可以用匿名对象来进行初始化)

问:如果说是vector<int>,int是内置类型,没有默认构造怎么办?

因为模板出来之后,为了解决这个问题,祖师爷让内置类型也有了默认构造(被迫升级),就是为了应对这个地方

整形就是0(char也是0,只不过在ascll中就是'\0'),浮点数就是0.0,指针就是空指针;

resize的逻辑:无非就是插入和删除的问题

因为我们reserve有做元素个数和容量大小的检查,所以这个地方我们可以直接分成两种情况(而不是三种情况)

插入:n比size大(无所谓capacity),我不管三七二十一,反正我就先扩容,反正实际扩不扩容还是得看reserve

删除:n比size小


总结就是以下

拷贝构造

因为编译器自动生成的拷贝构造走的是浅拷贝,所以当他们指向同一块空间时,在销毁时就会析构两次,就会报错,所以我们需要自己实现拷贝构造

(销毁的时候直接崩掉)

另一种拷贝构造的方法

push_back后会自己开空间和初始化,利用这个原理

(此处是this.push_back,不要疑惑)

可以利用把v1的值一次头插给v2来实现拷贝构造

但是这个拷贝构造还是不够好,还可以优化

(如果是string的话,范围for的时候会有很大消耗,所以说用引用;如果说要拷贝1000个数据,可以提前开空间)

赋值拷贝

现代写法

为了实现我们的现代写法赋值拷贝,我们提前搞一个swap来实现

但是还是报错了

我们这个swap期望是去调用库里面的,但是因为编译器在查找的时候会就近原则,他就会以为是递归那种自己调用自己,调自己的话就是一个参数,所以才报错说不接受两个参数

解决办法:

加上类域指定

构造(补充)

用迭代器构造

科普:如果说类的模板参数不够用,我还可以自己给自己加模板参数

可以利用其它对象的迭代器来初始化

调用的例子:

那我这样调用呢?想要从指向第二个迭代器和倒数第二个迭代器来进行初始化

会出问题:
begin和end是传值返回,所以是临时拷贝;不能加引用,防止会修改自己那个底层的成员

++和--的特点就是会去修改自身,所以编译器因为没办法修改临时拷贝而报错

解决办法:不改变就好了

也可以这样调用

迭代器构造写成模板的原因就是为了支持前面的写法

n个值的构造

但是会出错

原本希望调用n个值的构造,结果却调用到了迭代器构造;

划红圈的位置是报错的位置

找bug技巧:编译错误如果实在不好看,我们可以利用排除法(屏蔽掉部分代码进行确认

当屏蔽迭代器构造函数之后,可以发现,我们的n个值的构造函数是没有问题的

但是一旦两个同时出现的时候就会报错

以下的问题跟模板有关:

如果说10不用某些标识符来区分的话,默认就是int类型而不知sizt_t

此时第一个调用,10跟size_t  n不是特别匹配,但也能用;但是调用那个迭代器构造就非常合适了,所以这个构造就会调用迭代器的构造

在这个函数里面,*first的时候相当于(*10),想把地址编号为10的值进行尾插,所以就会报错说非法的间接寻址

第二个调用和第三个调用都是n个值构造比较适配,所以就不会出问题

解决办法:

库里面的解决办法是写一个重载,让特殊情况方便调用

{}构造(initializer_list)(C++11)

这部分内容只是提前看一下

原理:灵感来源于初始化数组

花括号类型:initializer_list<class>类

只有这几个成员方法

本质:这个类里面有两个指针,而{1, 2, 3, 4, 5, 6, 7, 8, 9}就是一个常量数组,在常量区开了一个空间,一个指针指向这个常量数组的开始lbegin,一个指向结束end 

通过sizeof计算这个花括号常量数组大小,得出来的大小是8byte(两个指针)也可以来证明这一点

有点像是vector,但是数据是在常量区,不能动态扩容       

所以下面的代码其实就是在用initializer_list对象来初始化vector(隐式类型转换)

所以说如果要实现用大括号来进行构造的话,就要有一个单参数构造函数,且 参数为initializer_list

initializer_list单参数构造函数

有迭代器就可以范围for

具体使用:

特殊bug

复杂vector情况

这样子会挂,关键在于开空间

通过检查,发现是析构的时候崩了

memcpy导致string浅拷贝了(所有的嵌套开辟空间的类型都会有这个问题)

当需要扩容的时候,会把原本的空间给delete[],同时也会调用每个string的析构函数,尽管mencpy对于vector是深拷贝,但是对于string仍然是浅拷贝,还是析构两次的问题

所以说必须要实现这样

问:解决方案可以是用swap吗?

不可以,因为不知道T是什么,所以也不知道T有没有swap,万一没有,调用了库里的浅拷贝一样完蛋

解决方案:不用mencpy了,直接for循环把原本空间的元素一个个赋值到新空间里

迭代器失效

情况1

这里的insert插入之后,导致扩容,然后原本的空间就失效了,it还是指向原本的空间,所以这个就是迭代器失效

问:之前我在解决这个东西的时候,不是遇到了这个问题,然后更新了pos吗?为什么还会失效?

因为形参是实参的拷贝,形参的修改不影响实参,it在外面不变

问:那为了解决这个问题,我传引用不就好了?

也不行,因为会导致以下的传参方式不行

begin是传值返回,具有常性,普通引用不能引用具有常性的值

解决办法:失效之后就不要再使用了,如果还想要找刚刚那个位置,那就自己找,自己去重新更新这个迭代器

情况2

erase的时候,会把后面的值覆盖前面的值,相当于把整体往后移了,相对的,自己就++了,所以会多移动一次

第一个对了是巧合

当最后一个数据是偶数就会出大问题(错过了判断条件)

就会让it一直往后越界,直到访问到没有开辟的空间就会崩了

问:那如果我选择删除的时候不动不就好了?

也不可以

  1. STL不强制要求具体实现,其他的vector具体的实现有可能缩容,缩容就意味着delete原空间之后开新空间,然后迭代器就失效了
  2. 因为vs规定,只要你前面进行了erase的话,后面就没办法用之前的迭代器了(因为vs用的不是原生指针,内部会有特殊处理和检查)

在erase之后,it就不能再用了,除非你新搞一个出来

解决办法:

既然有了这个问题,那么库里面自然会给出解决办法

会返回删除位置的下一个位置的迭代器

修改方案:

我们自己的erase也要修改

总结:只要进行了插入删除就要默认前面的迭代器已经失效了,因为STL没有规定具体实现,也就不知道什么时候会扩容然后换新空间

http://www.dtcms.com/a/293397.html

相关文章:

  • 基于SpringBoot+Vue的班级管理系统(Echarts图形化分析)
  • 一、Vue概述以及快速入门
  • DeepSeek下载量断崖式下跌72%,AI助手市场大洗牌 | AI早报
  • 广播分发中心-广播注册流程
  • 秋招Day17 - Spring - AOP
  • 构建RAG智能体(2):运行状态链
  • C#文件操作(创建、读取、修改)
  • 【世纪龙科技】电动汽车原理与构造-汽车专业数字课程资源
  • [c++11]final和override
  • 黄山派lvgl8学习笔记(2)导入头文件和新建一个按钮控件
  • 标记语言---XML
  • linux 驱动-power_supply 与 mtk 充电框架
  • 工业互联网时代,如何通过混合SD-WAN提升煤炭行业智能化网络安全
  • 【Pytorch】数据集的加载和处理(一)
  • 使用ubuntu:20.04和ubuntu:jammy构建secretflow环境
  • ndarray的创建(小白五分钟从入门到精通)
  • 嵌入式开发学习(第三阶段 Linux系统开发)
  • 数据资产——解读数据资产全过程管理手册2025【附全文阅读】
  • [c++11]constexpr
  • 考研数据结构Part1——单链表知识点总结
  • 陷波滤波器设计全解析:原理、传递函数与MATLAB实现
  • Netty中AbstractReferenceCountedByteBuf对AtomicIntegerFieldUpdater的使用
  • 威胁情报:Solana 开源机器人盗币分析
  • Automotive SPICE
  • git的版本冲突
  • 大模型——Data Agent:超越 BI 与 AI 的边界
  • 用ESP32打造全3D打印四驱遥控车:无需APP的Wi-Fi控制方案
  • 从0开始的中后台管理系统-2
  • 课题学习笔记2——中华心法问答系统
  • 汽车行业数字化——解读52页汽车设计制造一体化整车产品生命周期PLM解决方案【附全文阅读】