vector模板的简易实现

这篇文章,我们模拟一下STL里面的vector的实现。但是会简化一些内容,让大家能够更好的理解。模拟实现的目的不是为了更好的造轮子,而是为了更好的理解这些容器。
在这里插入图片描述

1. 成员变量

在前面的string里面,我们定义的成员变量是一个char类型的指针,然后是_size和_capacity,那么在vector里面是这样来定义的呢?我们可以看看vector的源代码。
在vector的源代码里它的成员变量是这样来定义的:
在这里插入图片描述
那么这里的迭代器是什么呢?
在这里插入图片描述
这个迭代器的意思是:模板类型的指针
那么我们在来看看这个迭代器的实现:
在这里插入图片描述
然后我们再看一下这个函数:
在这里插入图片描述
它的开始是start,结束是finish。相减为元素的个数。说明start是指向开头的指针,finish是指向最后一个元素的下一个位置的指针
那么end_of_storage是什么意思呢?
在这里插入图片描述
从这个函数可以分析出:end_of_storage减去开始,为容量大小。所以end_of_storage是指向总容量的下一个位置的指针
如下所示:
在这里插入图片描述
这样,我们可以先写出一些大概框架了:
在这里插入图片描述

2. push_back函数

和前面写string的思路是差不多的:
在这里插入图片描述
先看需不需要扩容。如果需要,扩完容就尾插。所以我们还需要写一个扩容的函数。

3. reserve函数

在这里插入图片描述
这样写有没有什么问题呢?这两行是有一些问题的:
在这里插入图片描述
在上一行代码_start被改成tmp了,这个size是_finish - 新的_start,然后在加上新的_start,所以还是_finish,还是nullptr。下面一行也是同样的道理。所以我们可以这样做:
在这里插入图片描述
就是提前把元素个数记录一下。然后我们再测试一下:
在这里插入图片描述

4. pop_back函数和下标运算符重载

这两个非常简单,都不多说什么了。
pop_back函数:
在这里插入图片描述
下标运算符重载:
在这里插入图片描述

5. resize函数

我们看一下库里的这个函数:
在这里插入图片描述
size_type是它自己typedef的,意思是:无符号整型。value_type也是重命名的,它的意思是:模板类型。那么我们自己写可以这样写:
在这里插入图片描述
第二个参数代表的是什么意思呢?就是我们不给参数的话,它会使用这个缺省值。而这个缺省值的意思是:创建一个临时对象,然后拷贝构造val。
那么有的同学会说:自定义类型具有构造函数,那么内置类型像int,double等类型咋办呢?
其实,C++中内置类型升级了,也可以认为有构造函数,析构函数。这样才能更好支持模板
在这里插入图片描述
剩下的思路和string哪里也是差不多的:
在这里插入图片描述

6. insert函数

我们先看一下库里的函数:
在这里插入图片描述
这里的pos的类型和返回值的类型是迭代器。为什么这里的返回值用的是迭代器呢?我们先写一个不带返回值的来分析:
在这里插入图片描述
那么这样写它有没有什么问题呢?我们来看:
在这里插入图片描述
经过测试程序挂了,我们调试一下:
在这里插入图片描述
一开始的数据。
在这里插入图片描述
扩完容的数据。我们发现_start和_finish的地址都变了,而pos没有变。因为扩容是把原来的空间销毁开辟新的空间,_start和_finish更新,pos没有更新。这就会导致下面的循环越界访问。所以我们要更新一下pos。
在这里插入图片描述
然后我们再测试一下:
在这里插入图片描述
这里就没有问题了。我们在说一下返回值的问题:
我们在这组数据中偶数的位置插入20。
在这里插入图片描述
但是出现了断言的错误。为什么呢?我们调试观察:
在这里插入图片描述
此时it来到2的位置,观察_start和_finish的地址。
在这里插入图片描述
此时扩完容之后,_start和_finish的地址都变了,it成为野指针了。等到后面遇到4的时候,又需要插入了,但是此时的it位置不对,是野指针。就会出现越界访问了。
那么有的同学会说:我们前面不是把pos的位置进行更新了吗?原因是:它是值传递,函数内部改变不会影响外部(it)。那么有的同学会说那我们改成引用。这是不行的,原因是:因为如果我们想传const对象或者是一个临时对象就不行了。所以我们需要返回值。
库里面的是这样返回的:指向新插入的第一个元素的迭代器
在这里插入图片描述
在这里插入图片描述
此时,我们每次插入把it更新一下。再测试一下:
在这里插入图片描述

7. erase函数

一般vector删除数据,都不考虑缩容的方案。缩容方案:size<capacity/2时,可以考虑开一个size()大小的空间,拷贝数据,释放旧空间。缩容方案的本质是时间换空间。一般设计都不会考虑缩容。
那么不考虑缩容,我们erase要不要返回值呢?
我们看一下库里面的erase函数的格式:
在这里插入图片描述
它的返回值:
在这里插入图片描述
指向函数调用删除的最后一个元素后面的元素的新位置的迭代器。如果操作擦除了序列中的最后一个元素,则此操作为容器结束

为什么呢?我们来分析一下:
在这里插入图片描述
在这里,我们使用的是库里面的vector的erase函数。我们删除的是序列中的最后一个元素。那么此时pos指向的就是_finish,我们下面再去访问就会越界。这时有的同学会说如果我们不删除最后一个元素呢?
在这里插入图片描述
我们发现程序还是挂了。
这是因为:erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了

我们来实现一下erase函数:
在这里插入图片描述
这里的pos位置就是删除元素的下一个位置。
我们这里的实现,并没有像vs那样强制检查pos位置。所以不会出现程序崩溃的情况。

总结:vector迭代器失效有两种。1.增容,缩容,导致野指针失效。2.迭代器指向的位置意义变了。

8. Linux下迭代器失效问题

Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端。

我们先看insert扩容情况下
在这里插入图片描述
在这里插入图片描述
扩容之后,迭代器已经失效了,在vs下程序就直接崩溃了,但是linux下不会。虽然能运行,但是输出的结果是不对的

erase删除情况下
在这里插入图片描述
在这里插入图片描述
erase删除任意位置代码后,linux下迭代器并没有失效。因为空间还是原来的空间,后序元素往前搬移了,it的位置还是有效的

总结:对于insert和erase造成的迭代器失效问题。Linux的g++检查不严,基本依靠操作系统自身野指针越界检查机制。windows下vs系列检查更严格,使用一些强制检查机制,意义变了也可能会检查出来。

9. 构造函数和析构函数和赋值函数

析构函数比较简单
在这里插入图片描述
构造函数
上面我们已经写了一个无参的构造函数。但是它还有一些其它的构造方式,我们先来看看库的构造函数:
在这里插入图片描述
库里面的构造函数有三种。第一种的就是无参的(暂时不考虑分配器 )。那么我们还有两种的构造方式。
第二个是:构造n个val值的vector
在这里插入图片描述
然后我们再来测试一下:
在这里插入图片描述
第三个是:用迭代器区间来构造vector
在这里插入图片描述
我们来测试一下:
在这里插入图片描述
我们发现没有问题。但是我们再测试一下test8()。
在这里插入图片描述
我们看到报错了。这是为什么呢?原因是:10是int类型,而我们写的第二种是size_t类型,不太符合(有的编译器会类型转换,vs2019没有)。所以vs2019中就不会匹配第二种构造,而是去匹配第三种构造。而第三种构造中,10是int类型,*first是不能解引用int类型的。所以报错了。那么我们该怎么办呢?我们可以再重载一个构造函数:
在这里插入图片描述
我们重载一个int类型的,其它不变。就可以了。

拷贝构造函数
在这里插入图片描述
拷贝构造函数,我们这里就不写传统写法了。直接写现代写法:
在这里插入图片描述
赋值函数
在这里插入图片描述
这里我们也只写现代写法:
在这里插入图片描述
我们再测试一下:
在这里插入图片描述

10. 使用memcpy拷贝问题

模拟实现的vector中的reserve接口中,使用memcpy进行的拷贝,以下代码会发生什么问题?
在这里插入图片描述
我们这里写了一个杨辉三角的类。创建了一个5行的杨辉三角。我们来运行一下:
在这里插入图片描述
我们看到,它出现了随机数。这是什么情况呢?
在这里插入图片描述
其实这一步,要进行两次的拷贝构造。但是在某些编译器的优化下,可能进行一次拷贝构造。但是还是需要进行拷贝的。然后,我们调试进入拷贝构造里面观察。
在这里插入图片描述
此时,我们再进入构造函数里面观察。这个构造函数是由迭代器区间来构造的,所以进去的是这个构造函数:
在这里插入图片描述
然后,我们需要进入push_back函数。
在这里插入图片描述
因为第一次我们是没有空间的,直接开辟4个大小的空间。然后将前4行数据一个一个插入到tmp空间里。如下图所示:
在这里插入图片描述
但是当我们插入第5行数据时,需要扩容。
在这里插入图片描述
此时,我们已经开辟了一个大小为5的空间。memcpy的拷贝实际是浅拷贝,所以两个指向的是同一块空间。
在这里插入图片描述
此时又要delete[] _start,所以它会析构里面的vector< int >,而自定义类型销毁时会去调用它的析构函数,所以都会被析构。然后_start和_finish指向新的空间:
在这里插入图片描述
此时再插入第5行数据。
在这里插入图片描述
但是,我们要知道。前面的4行数据已经被销毁,成为随机值了。
在这里插入图片描述
此时tmp再和我们要拷贝构造的对象(ret)交换。
在这里插入图片描述
当这个函数结束时,ret也会销毁,销毁就会去调用析构函数。所以会报错。
解决方案:
在这里插入图片描述
这样赋值就不会出现问题了。如果是内置类型,那么直接赋值,肯定没有问题。如果是自定义类型不带资源管理的,那么赋值为浅拷贝也是可以的。如果是自定义类型带资源管理的,那么赋值就会用深拷贝。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学代码的咸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值