【C++】手把手教你模拟实现vector

news/2024/7/5 4:29:30

vector模拟实现

    • 前言
    • 正式开始
      • 三个成员变量
      • 无参构造
      • 析构
      • push_back
      • [ ]重载
      • pop_back
      • insert
      • erase
      • 迭代器失效问题
        • insert迭代器失效
        • erase迭代器失效
      • 深浅拷贝
        • 拷贝构造函数
        • 赋值运算符重载
      • n个val构造
      • resize
      • front和back
        • front
        • back

在这里插入图片描述

前言

这篇写的是vector的模拟实现。

如果对于vector不熟悉的话,可以看前一篇博客:vector基本用法介绍

本篇不会讲太多实现上的细节,会偏重迭代器方面的讲解,如果想要更好理解构造函数、[]运算符重载等函数的实现的话,可以看这篇string的模拟实现,因为string的用法和vector是差不多的:手把手教你模拟实现string类

正式开始

模拟实现,首先要搞清楚其主要的成员变量有啥。

其实就3个:start、finish、end_of_storage。
这三个都是指针,start指向所开辟的空间的首地址,finish指向实际存储元素的下一个位置,end_of_storage指向开辟空间的末尾的下一个位置。
在这里插入图片描述

前一篇介绍vector的博客也说了,vector是一个模板类。
模板参数T,还有一个内存池。
但是这里实现的话,就不搞内存池了,先把一些基本的搞懂,再说那内存池的东西,后面自然会说的。

然后库中的实现,有type_name什么我们未接触到STL时不懂的类型,但是上一篇也说了,这篇就讲了,直接用,但是把库中的实现拿出来看一下:
在这里插入图片描述

库中对于T*,搞了两个类型,一个是pointer,一个时iterator。

我们就用后面那个就行。这一个iterator就是一个T而已,前面也是说过了, vector和string这种空间是连续的数据结构,其迭代器底层就是原生的指针。我们用iterator就用的是T,如果T是int的话,就是int*。

废话不多说,开搞。

三个成员变量

先把迭代器搞出来,非常简单,不要觉得听起来很高大上,就长这样:
在这里插入图片描述
然后在搞三个成员变量:
在这里插入图片描述
前面那张图也提到了,这里再放出来理解一下:
在这里插入图片描述
然后就是构造。

无参构造

先看一下库中是怎么实现的:
在这里插入图片描述
给的是0,就是空指针nullptr。

我们也写一下:
在这里插入图片描述

不多赘述,接着看析构:

析构

因为三个成员变量都是指针,且指向同一块空间,所以释放最前面的那个指针就行了。
在这里插入图片描述
想要简单使用的话,在搞一下尾插尾删就可以了。

push_back

尾插的话,是增加元素,只要增加,第一时间想到的就是扩容。
扩容的话,我们需要知道size和capacity的确切值。
也很简单,指针减去指针就可以了:
size: _finish - _start 就是size。
capacity: _end_of_storage - _start 就是capacity。

然后只要size和capacity相等的时候就要扩容,初始情况下,二者都为nullptr也成立。

然后光是上面的这些话,就要写三个函数:size()、capacity()、reserve()。
分别是size、capacity和扩容。

代码具体怎么实现的就不说了,前面C语言写顺序表的时候已经说过了,这里就不把时间放在这种基本功上面的了。

先实现一下:
在这里插入图片描述
其实上面的扩容是有bug的,不知道细心的同学发现了没有。

当我们扩完了容之后,如果_start不是是nullptr就需要将原来的_start中的数据转到tmp中,再改变_finish,但是改finish的时候调用size(),进入到size函数中_finish - _start,此时_start已经变了,但是finish并没有变,所以把size()替换为_finish - _start后,整个式子就变成了_finish = _statr + _finish - _start = _finish。故当我们扩了容之后finish并没有改变,从头到尾一直是nullptr。所以就出问题了。

我们把push_back写完后来试试:
在这里插入图片描述

在这里插入图片描述
可以看到,出了问题,就是finish一直是那个nullptr,变不了,那么怎么改呢?

两种方法:

  1. 将_start的赋值语句和_finish的赋值语句调换下位置,并将_finish赋值语句中的_start变为tmp。
    在这里插入图片描述
    此时再调试,就可以了:
    在这里插入图片描述

  2. 在前面将未改变的size记录下来,并将_finish赋值语句中的size()改变为记录下来的原始size。
    在这里插入图片描述

只要在_start改变之前记录下来就可以了。

再调试,也可以:
在这里插入图片描述

更推荐第二种写法。

[ ]重载

再实现下[]操作符重载。
前面库中vector的成员类型有个reference,就是引用,我们也写一个。用来实现[]重载。
在这里插入图片描述
用一下:
在这里插入图片描述
但是如果是const对象的话,就不能用了。
所以也要写一个const的[]重载:
在这里插入图片描述

想要遍历顺序表中的元素的话,前一篇也说了,三种方法。

  1. 前面的for循环
  2. 迭代器
    迭代器前面也定义了。就是T*。
    然后搞一下begin和end:
    在这里插入图片描述
    很简单,就是原生指针。没有太大的含金量。

然后我们用这个遍历试一下:
在这里插入图片描述
和库中的用法是一样的。

  1. 范围for
    有了迭代器就可以用范围for了,因为范围for底层就是迭代器的“傻瓜式”替换。

看:
在这里插入图片描述
但是如果我们把begin和end函数名改一下,范围for就用不了了。

比如说第一个字母给大写:
在这里插入图片描述

此时范围for报错:
在这里插入图片描述
所以说范围for底层就是迭代器的傻瓜式的替换。只认识begin和end,字母变一下就不认识了。

然后库中也有const对象对应的迭代器,这里也实现一下:
在这里插入图片描述

在这里插入图片描述
调用一下:
在这里插入图片描述
在这里插入图片描述

这里const对象也是变一下begin和end就会范围for就会失效。
正常情况下:
在这里插入图片描述
变了begin/end后:
在这里插入图片描述
又不能用了。

这个讲到这。
下面说pop_back。

pop_back

很简单,还是要先考虑是否为空,然后_finish减一下就行。

在这里插入图片描述

在这里插入图片描述

然后说insert和erase。

insert

很简单,string中也是讲过的,不说细节了,直接给代码:
在这里插入图片描述

这里的insert要配合着find来使用。
在这里插入图片描述

erase

库中的erase两个函数返回值都是Iterator,但是insert有的是返回Iterator,有的是void,所以说这里我就只实现了一个返回Iterator。

也是直接给实现:
在这里插入图片描述
再调用一下:
在这里插入图片描述

但是上面的有些要注意的地方。
就是迭代器失效。

迭代器失效问题

insert迭代器失效

先说insert的迭代器失效问题:
看代码:
在这里插入图片描述
这里直接崩掉了。
原因很简单,就是我们在pos位置插入时,pos位置已经失效了。
为什么pos位置会失效呢?
注意看,顺序表中本来就只有1、2、3、4四个元素,所以说当我们插入数据时会扩容,如果扩容,那么原空间地址就会失效,换成新的地址,此时pos位置仍然指的是原空间的地址,所以说,扩完容后再插入,就会非法访问源地址空间,此时就会出错,这就是所谓的迭代器失效。

那么怎么避免呢?
也很简单,扩了容之后将pos位置进行修正就可以了。
看:
在这里插入图片描述

再测试:
在这里插入图片描述
此时就没事了,但只是这里没事了,我们再来搞一个测试。

pos位置处插入数据后,再到pos位置处插入数据:
在这里插入图片描述
断言崩掉了。

这里还是因为pos,我们是传值传的pos,insert函数中的pos改了,但是外面的pos不会改变,第一次传过去,pos还是在那个合法范围中(start ~ finish)的,第二次传过去pos还是原来的那个pos,就完全不在合法范围了。

有的同学可能就说为什么不将参数改为引用呢?
那么我只能说,当我传参是begin()或者end()时,你又该如何应对?

传参为普通的指针时,可以通过:
在这里插入图片描述

如果改为引用:

虽然pos可以传:
在这里插入图片描述

但是如果我传begin的话:

在这里插入图片描述
就传不过去了。

因为begin返回时不是返回_start本身,而是返回一个临时数据,临时数据具有常属性,不能被修改,如果传给引用成功的话,就会导致权限被放大,导致临时数据能被修改,所以就不能传引用了。

那又有同学说能不能将参数改为加const的呢?
还是不能,因为加了const后,你insert里面的那个pos怎么修改呢?
所以说是不行的。

库中的实现,参数也是Iterator就完了,没有加引用或者const。
在这里插入图片描述
说一点最重要的:当使用pos插入了元素之后,就尽量不要再使用pos访问那个位置了。

我上面模拟实现的insert是有点小瑕疵的。返回值要改为返回一下插入位置的迭代器。
代码如下:
在这里插入图片描述

那么insert就讲到这,下面说erase的迭代器失效。

erase迭代器失效

仔细看我的erase模拟实现。是不会出现迭代器失效的问题的。
但是不能说库中的erase不会失效。

这里的模拟实现并没有进行缩容的操作,缩容就是指当实际元素个数小于容量的
一半或者多少倍时,就将顺序表的容量进行缩减,一般不会这样做,但是STL并没有规定说不能这样做,可能有的库中会这样进行实现,如果进行缩容了的话,原空间丢失,就又会导致insert中的迭代器pos位置失效。

这里就不演示缩容的了,这种情况很少见,大家只要懂了上面insert中的迭代器失效,相信这里也是能懂erase失效的。

那就展示个别的,删除所有偶数:
给出如下代码:

auto it = v1.begin();
// 删除所有偶数
while (it != v1.end())
{
	if (*it % 2 == 0)
	{
		v1.erase(it);
	}

	++it;
}

上面的代码中给出v1三种情况:

  1. 1、2、3、4、5
  2. 1、2、3、4
  3. 1、2、4、3、4、5

三种情况,结果各不相同。
因为上面的代码有问题。

先把挨个的结果给出来:

第一种:结果正确。
在这里插入图片描述

第二种:程序崩溃。
在这里插入图片描述

第三种:结果错误。
在这里插入图片描述

为什么?
我只能说大家画一下图,自己模拟一下删除的过程。
我这里也不好演示,能力有限,不会搞那种动图。

第一种情况是凑巧,歪打正着。
第二种情况是越界,程序崩掉了。
第三种情况是,将偶数直接跳过了。

那么把上面的代码改改就行。

库中的erase是有返回值的,都是返回删除位置的下一个数据的迭代器的位置。

那么就好说了,我们每次删除一个数据之后就更新一下当前迭代器的位置就解决了。

代码如下:

auto it = v1.begin();
while (it != v1.end())
{
	if (*it % 2 == 0)
	{
		it = v1.erase(it);
	}
	else
	{
		++it;
	}
}

这样就好了。
上面的三个场景都是正确的。

总结一下:
insert / erase pos位置,不要直接访问,一定要更新,直接访问可能出现各种各样的结果,就是所谓的迭代器失效。

STL只是一个规范,只规定了要实现什么东西,具体的实现细节没有做什么要求,不同的平台下的实现是不同的。
上面的三个示例,g++和vs2019下结果一样,但是vs2013下三种情况都会报错。就是因为库的实现不同。

下面说一说深浅拷贝的问题。

深浅拷贝

深浅拷贝对于自定义类型来说已经是家常便饭了。
老问题。浅拷贝的结果是:1. 析构两次, 2. 一个对象修改同时影响多个对象。

深浅拷贝存在于拷贝构造函数和赋值运算符重载中。挨个说。

拷贝构造函数

这里给三种实现方法。

  1. 正常实现

直接给代码:
在这里插入图片描述
注意上面const对象不能调用非const成员函数,所以还要再写一个const的size()。

测试一下:
在这里插入图片描述

  1. 复用reserve和push_back
    在这里插入图片描述
    测试一下:
    在这里插入图片描述

但其实,代码是稍微有点问题的。
vs2019会自动初始化成员变量,如果别的平台没有初始化的话,就会有问题。
没有初始化的话,_start就会变为野指针,当reserve的时候就会先拷贝脏数据然后再释放未开辟的空间,就会导致程序崩溃。
所以说得加上初始化:
在这里插入图片描述
这样才比较完善。

  1. 复用构造再交换
    我前面在模拟实现string那篇中也用到了这个方法,但是string有个构造函数是可以多个元素初始化成不同数据的,但vector中只有个迭代器区间初始化时可以实现这样的初始化,所以我们要先实现以下这个迭代器区间的构造函数。

迭代器区间我们可以直接用我们自己写的那个iterator,但是也可以用一个函数模板来实现,后者的好处在于能够用不同类的迭代器区间来构造类对象。可能你没有听懂,没关系,等会给示例就懂了。
像下面这样的迭代器区间:
在这里插入图片描述

实现出来就是这样:
在这里插入图片描述

测试一下:
在这里插入图片描述

也是有点问题,没有初始化。但是vs2019会默认初始化。
加上:
在这里插入图片描述

然后我们试一下刚才说的不同类型初始化。
在这里插入图片描述
就是上面的这个。

然后就是拷贝构造函数:
在这里插入图片描述

上面拷贝构造函数中swap没有用库中的,因为直接用库中的话是深拷贝开销会很大,没必要,自己实现一个就好。

测试一下:
在这里插入图片描述
再来说一下赋值运算符重载。

赋值运算符重载

还是先给出普通的实现:

在这里插入图片描述

测试下:
在这里插入图片描述

再用一个比较简便的方式:
在这里插入图片描述
测试一下:
在这里插入图片描述

可能基础不牢的同学这个方法已经看懵了,不要懵,听我讲。

v3 = v2。v2以传值的形式传给了v,所以说v是v2的一份拷贝,将这个拷贝的数据与v3进行交换,就能把v中与v2相同的值全部交换给v3,这样就能达成赋值的目的,然后重载函数运行完毕,栈帧销毁,v被释放,不会造成任何影响还能给v3赋值。

然后再来看个构造函数。

n个val构造

直接给代码:
在这里插入图片描述

测试一下:
在这里插入图片描述
细心的同学可能已经发现了v1初始化的时候用的是8u,也就是无符号整型8。
但是我为什么要这么做呢?

看一下我不加u的结果:
在这里插入图片描述
出错了,编译器说我非法的间接寻址。

为什么?
直接说,就是因为前面我写的那个迭代器的构造。
这里参数是两个int,也就是(int, int)。
编译器在匹配重载的函数的时候,是按照最匹配的函数来进行匹配的。
我们的迭代器区间构造函数,参数是(InputIterator, InputIterator)这两个一样的参数,也就是说,我们没有参数为 (int, int) 的构造函数,所以说当我们传参为int,int最匹配的就是(InputIterator, InputIterator),而不是(size_t n, const T& val = T()),所以此时调用的就是迭代器区间的构造函数。

那么如何避免这个问题呢?
我看库里面的实现是再专门搞一个参数为(int, const T&)的,甚至还有个(long, const T&):
在这里插入图片描述
所以我们模拟的话,就也专门搞一个参数为(int, const T&)的构造函数就行。
在这里插入图片描述
测试一下:
在这里插入图片描述
完全ok。

然后看一下resize()这个函数。

resize

就考虑三种情况就行。

  1. n > capacity
    就是扩容加初始化

  2. size < n < capacity
    就是初始化

  3. n < size
    就是缩容

在这里插入图片描述

测试一下:

代码如下:

	vector<int> v1(8, 5);
	v1.resize(10);
	v1.resize(5);
	v1.resize(8);

上面的情况分别打印出来就是:
在这里插入图片描述
我们再看一下标准库中的和我们的一样不:
在这里插入图片描述
一样。

然后再看一下front和back。

front和back

很简单,返回值就是首尾元素。而且是引用。

front

在这里插入图片描述
测试一下:
在这里插入图片描述

back

在这里插入图片描述
测试一下:
在这里插入图片描述

然后再讲点关于深拷贝的问题。
不知道各位看我前一篇vector简介没,那篇最后我给了一道题,就是杨辉三角,这里需要用一下这个。

就是二维数组,用vector实现的二维数组。

借用一下那道题的代码:

class Solution {
public:
	vector<vector<int>> generate(int numRows) {
		vector<vector<int>> res;
		res.resize(numRows);
		for (int i = 0; i < res.size(); ++i)
		{
			res[i].resize(i + 1);
			res[i].front() = 1;
			res[i].back() = 1;
		}

		for (int i = 1; i < res.size(); ++i)
		{
			for (int j = 1; j < res[i].size() - 1; ++j)
			{
				if (res[i][j] == 0)
				{
					res[i][j] = res[i - 1][j] + res[i - 1][j - 1];
				}
			}
		}

		return res;
	}
};

在这里插入图片描述
这里运行起来的话,会直接崩掉。
在这里插入图片描述

调试起来发现是最后析构的时候错了。
在这里插入图片描述

看一下返回值:
在这里插入图片描述

如果我们把返回值改为void会怎么样:
在这里插入图片描述
运行起来了。

然后我们再来搞一个顺序表来接收一下最后的二维数组。
在这里插入图片描述

调试起来最终还是在析构处崩掉了。
在这里插入图片描述
不卖关子了,讲:

我们调试起来发现:
在这里插入图片描述
虽然外层的vector是深拷贝。
但是内层的那个vector是浅拷贝:
在这里插入图片描述
画出来图的话,就是这样:
在这里插入图片描述
蓝色区域的部分,在析构的时候会被释放两次。

此时就会导致程序崩溃。
怎么避免呢?
看一下我们的拷贝构造函数:

在这里插入图片描述

memcpy不能用,内部的成员会发生浅拷贝。因为memcpy是逐字节的拷贝。拷贝完后的数据是完全一样的。_start[i] = v._start[i]就不是了,当_start是vector的时候就会去先调用拷贝构造来对_start内部的成员赋值,然后再调用赋值重载,给_start整体赋值。

所以得要换成for循环来逐个赋值。
在这里插入图片描述
这样的话就能实现深拷贝。

也就是这样:
在这里插入图片描述

上面的拷贝构造用的是普通版本的拷贝构造。
我们的reserve也是有问题的。也要改成for循环的。
在这里插入图片描述
我们其他的写法就是用了reserve,如果没有改reserve的话就会崩掉。

再看一下二维的顺序表的图:
在这里插入图片描述

就讲到这吧,这个模拟实现讲的挺多的了。

到此结束。。。


http://lihuaxi.xjx100.cn/news/1327192.html

相关文章

ruoyi若依 组织架构设计--[ 部门管理 ]

ruoyi若依 组织架构设计--[ 部门管理 ] 部门管理部门查询部门新增部门修改部门删除 部门管理 部门查询 需要注意的是&#xff0c;部门管理也有数据权限&#xff0c;比如A用户分配的数据权限(通过角色分配)是深圳总公司&#xff0c;那么A用户登录后看到的部门也是深圳总公司&am…

SpringBoot SpringMVC整合prometheus(consoul)

Promethus consoul Spring boot 整合 prometheus 传统 Spring MVC 项目 集成 Prometheus

【笔记】数字电路基础1 - 门电路

目录 数字电路基础与门电路数电基础基本门电路复合门电路TTL 门电路CMOS 门电路 数字电路基础与门电路 数电基础 数字电路中常将 0 &#xff5e; 1V 范围的电压称为低电平&#xff0c;用“0”表示&#xff1b;而将 3 &#xff5e; 5V 范围的电压称为高电平&#xff0c;用“1”…

springboot+vue膳食营养健康网站零食美食品商城_4d8g9

随着社会的不断进步与发展&#xff0c;人们对生活质量要求逐步提升。如果开发一款膳食营养健康网站&#xff0c;可以让用户在最短的时间里享受到最好的服务&#xff1b;而开发本网站&#xff0c;又能够提高网站整体工作水平&#xff0c;简化工作程序&#xff0c;这对管理员和用…

Java的JPAMyBatis Plus优缺点

JPA框架 优点&#xff1a; 简化数据库操作&#xff1a;JPA框架通过对象关系映射&#xff08;ORM&#xff09;技术&#xff0c;将Java对象映射到数据库表&#xff0c;可以通过简单的操作Java对象来进行数据库的增删改查操作&#xff0c;无需编写复杂的SQL语句。提高开发效率&a…

生产环境 kafka 平滑迁移之旅

文章目录 背景分析测试环境验证现实很残酷两种抉择-----leader分区切换方案选择实施步骤手工副本集增加步骤手工leader分区切换步骤 总结 背景 线上kafka集群&#xff0c;3台机器&#xff0c;3个broker&#xff1b;其中某台机器因为硬件故障&#xff0c;需要停机维修&#xff…

tensorboard 如何导出数据

tensorboard 如何导出数据 场景描述&#xff1a;有时候在第一遍跑实验的时候&#xff0c;由于epoch和内部循环变量的原因&#xff0c;做出来的图可能不是我们想要的&#xff0c;这个时候&#xff0c;需要自己导出数据并且重新画图&#xff0c;本文介绍如何导出数据道json或csv格…

【Step By Step】VM安装redhat-server7.9搭建Oracle19C RAC(一)环境配置

文章目录 环境规划网络规划文件系统规划rac用户规划grid与oracle用户规划ASM规划 虚拟机设置搭建虚拟机自定义网卡安装操作系统 操作系统设置关闭services修改/etc/hosts创建用户与组创建文件目录设置环境变量设置内核参数资源限制添加 etc/pam.d/login关闭大页关机挂载本地ISO…