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

vector模拟实现

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

【C++】手把手教你模拟实现vector_第1张图片

前言

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

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

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

正式开始

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

其实就3个:start、finish、end_of_storage。
这三个都是指针,start指向所开辟的空间的首地址,finish指向实际存储元素的下一个位置,end_of_storage指向开辟空间的末尾的下一个位置。
【C++】手把手教你模拟实现vector_第2张图片

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

有type_name什么我们刚接触到STL时不懂的类型,上一篇vector介绍时也说了,就是一些typedef的类型,把库中的实现拿出来看一下:
【C++】手把手教你模拟实现vector_第3张图片

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

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

废话不多说,开搞。

三个成员变量

先把迭代器搞出来,非常简单,不要觉得听起来很高大上,就长这样:
【C++】手把手教你模拟实现vector_第4张图片
然后在搞三个成员变量:
【C++】手把手教你模拟实现vector_第5张图片
前面那张图也提到了,这里再放出来理解一下:
【C++】手把手教你模拟实现vector_第6张图片
然后就是构造。

无参构造

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

我们也写一下:
【C++】手把手教你模拟实现vector_第7张图片

不多赘述,接着看析构:

析构

因为三个成员变量都是指针,且指向同一块空间,所以释放最前面的那个指针就行了。
【C++】手把手教你模拟实现vector_第8张图片
想要简单使用的话,在搞一下尾插尾删就可以了。

push_back

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

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

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

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

先实现一下:
【C++】手把手教你模拟实现vector_第9张图片
其实上面的扩容是有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写完后来试试:
【C++】手把手教你模拟实现vector_第10张图片

【C++】手把手教你模拟实现vector_第11张图片
可以看到,出了问题,就是finish一直是那个nullptr,变不了,那么怎么改呢?

两种方法:

  1. 将_start的赋值语句和_finish的赋值语句调换下位置,并将_finish赋值语句中的_start变为tmp。
    【C++】手把手教你模拟实现vector_第12张图片
    此时再调试,就可以了:
    【C++】手把手教你模拟实现vector_第13张图片

  2. 在前面将未改变的size记录下来,并将_finish赋值语句中的size()改变为记录下来的原始size。
    【C++】手把手教你模拟实现vector_第14张图片

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

再调试,也可以:
【C++】手把手教你模拟实现vector_第15张图片

更推荐第二种写法。

[ ]重载

再实现下[]操作符重载。
前面库中vector的成员类型有个reference,就是引用,我们也写一个。用来实现[]重载。
【C++】手把手教你模拟实现vector_第16张图片
用一下:
【C++】手把手教你模拟实现vector_第17张图片
但是如果是const对象的话,就不能用了。
所以也要写一个const的[]重载:
【C++】手把手教你模拟实现vector_第18张图片

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

  1. 前面的for循环
  2. 迭代器
    迭代器前面也定义了。就是T*。
    然后搞一下begin和end:
    【C++】手把手教你模拟实现vector_第19张图片
    很简单,就是原生指针。没有太大的含金量。

然后我们用这个遍历试一下:
【C++】手把手教你模拟实现vector_第20张图片
和库中的用法是一样的。

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

看:
【C++】手把手教你模拟实现vector_第21张图片
但是如果我们把begin和end函数名改一下,范围for就用不了了。

比如说第一个字母给大写:
【C++】手把手教你模拟实现vector_第22张图片

此时范围for报错:
【C++】手把手教你模拟实现vector_第23张图片
所以说范围for底层就是迭代器的傻瓜式的替换。只认识begin和end,字母变一下就不认识了。

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

【C++】手把手教你模拟实现vector_第24张图片
调用一下:
【C++】手把手教你模拟实现vector_第25张图片
【C++】手把手教你模拟实现vector_第26张图片

这里const对象也是变一下begin和end就会范围for就会失效。
正常情况下:
【C++】手把手教你模拟实现vector_第27张图片
变了begin/end后:
【C++】手把手教你模拟实现vector_第28张图片
又不能用了。

这个讲到这。
下面说pop_back。

pop_back

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

【C++】手把手教你模拟实现vector_第29张图片

【C++】手把手教你模拟实现vector_第30张图片

然后说insert和erase。

insert

很简单,string中也是讲过的,不说细节了,直接给代码:
【C++】手把手教你模拟实现vector_第31张图片

这里的insert要配合着find来使用。
【C++】手把手教你模拟实现vector_第32张图片

erase

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

也是直接给实现:
【C++】手把手教你模拟实现vector_第33张图片
再调用一下:
【C++】手把手教你模拟实现vector_第34张图片

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

迭代器失效问题

insert迭代器失效

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

那么怎么避免呢?
也很简单,扩了容之后将pos位置进行修正就可以了。
看:
【C++】手把手教你模拟实现vector_第36张图片

再测试:
【C++】手把手教你模拟实现vector_第37张图片
此时就没事了,但只是这里没事了,我们再来搞一个测试。

pos位置处插入数据后,再到pos位置处插入数据:
【C++】手把手教你模拟实现vector_第38张图片
断言崩掉了。

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

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

传参为普通的指针时,可以通过:
【C++】手把手教你模拟实现vector_第39张图片

如果改为引用:

虽然pos可以传:
【C++】手把手教你模拟实现vector_第40张图片

但是如果我传begin的话:

【C++】手把手教你模拟实现vector_第41张图片
就传不过去了。

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

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

库中的实现,参数也是Iterator就完了,没有加引用或者const。
【C++】手把手教你模拟实现vector_第42张图片
说一点最重要的:当使用pos插入了元素之后,就尽量不要再使用pos访问那个位置了。

我上面模拟实现的insert是有点小瑕疵的。返回值要改为返回一下插入位置的迭代器。
代码如下:
【C++】手把手教你模拟实现vector_第43张图片

那么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

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

先把挨个的结果给出来:

第一种:结果正确。
【C++】手把手教你模拟实现vector_第44张图片

第二种:程序崩溃。
【C++】手把手教你模拟实现vector_第45张图片

第三种:结果错误。
【C++】手把手教你模拟实现vector_第46张图片

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

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

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

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

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

代码如下:

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

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

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

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

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

深浅拷贝

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

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

拷贝构造函数

这里给三种实现方法。

  1. 正常实现

直接给代码:
【C++】手把手教你模拟实现vector_第47张图片
注意上面const对象不能调用非const成员函数,所以还要再写一个const的size()。

测试一下:
【C++】手把手教你模拟实现vector_第48张图片

  1. 复用reserve和push_back
    【C++】手把手教你模拟实现vector_第49张图片
    测试一下:
    【C++】手把手教你模拟实现vector_第50张图片

但其实,代码是稍微有点问题的。
vs2019会自动初始化成员变量,如果别的平台没有初始化的话,就会有问题。
没有初始化的话,_start就会变为野指针,当reserve的时候就会先拷贝脏数据然后再释放未开辟的空间,就会导致程序崩溃。
所以说得加上初始化:
【C++】手把手教你模拟实现vector_第51张图片
这样才比较完善。

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

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

实现出来就是这样:
【C++】手把手教你模拟实现vector_第53张图片

测试一下:
【C++】手把手教你模拟实现vector_第54张图片

也是有点问题,没有初始化。但是vs2019会默认初始化。
加上:
【C++】手把手教你模拟实现vector_第55张图片

然后我们试一下刚才说的不同类型初始化。
【C++】手把手教你模拟实现vector_第56张图片
就是上面的这个。

然后就是拷贝构造函数:
【C++】手把手教你模拟实现vector_第57张图片

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

测试一下:
【C++】手把手教你模拟实现vector_第58张图片
再来说一下赋值运算符重载。

赋值运算符重载

还是先给出普通的实现:

在这里插入图片描述

测试下:
【C++】手把手教你模拟实现vector_第59张图片

再用一个比较简便的方式:
【C++】手把手教你模拟实现vector_第60张图片
测试一下:
【C++】手把手教你模拟实现vector_第61张图片

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

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

然后再来看个构造函数。

n个val构造

直接给代码:
【C++】手把手教你模拟实现vector_第62张图片

测试一下:
【C++】手把手教你模拟实现vector_第63张图片
细心的同学可能已经发现了v1初始化的时候用的是8u,也就是无符号整型8。
但是我为什么要这么做呢?

看一下我不加u的结果:
【C++】手把手教你模拟实现vector_第64张图片
出错了,编译器说我非法的间接寻址。

为什么?
直接说,就是因为前面我写的那个迭代器的构造。
这里参数是两个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&)的构造函数就行。
【C++】手把手教你模拟实现vector_第65张图片
测试一下:
【C++】手把手教你模拟实现vector_第66张图片
完全ok。

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

resize

就考虑三种情况就行。

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

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

  3. n < size
    就是缩容

【C++】手把手教你模拟实现vector_第67张图片

测试一下:

代码如下:

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

上面的情况分别打印出来就是:
【C++】手把手教你模拟实现vector_第68张图片
我们再看一下标准库中的和我们的一样不:
【C++】手把手教你模拟实现vector_第69张图片
一样。

然后再看一下front和back。

front和back

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

front

在这里插入图片描述
测试一下:
【C++】手把手教你模拟实现vector_第70张图片

back

【C++】手把手教你模拟实现vector_第71张图片
测试一下:
【C++】手把手教你模拟实现vector_第72张图片

然后再讲点关于深拷贝的问题。
不知道各位看我前一篇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;
	}
};

【C++】手把手教你模拟实现vector_第73张图片
这里运行起来的话,会直接崩掉。
【C++】手把手教你模拟实现vector_第74张图片

调试起来发现是最后析构的时候错了。
【C++】手把手教你模拟实现vector_第75张图片

看一下返回值:
【C++】手把手教你模拟实现vector_第76张图片

如果我们把返回值改为void会怎么样:
【C++】手把手教你模拟实现vector_第77张图片
运行起来了。

然后我们再来搞一个顺序表来接收一下最后的二维数组。
【C++】手把手教你模拟实现vector_第78张图片

调试起来最终还是在析构处崩掉了。
【C++】手把手教你模拟实现vector_第79张图片
不卖关子了,讲:

我们调试起来发现:
【C++】手把手教你模拟实现vector_第80张图片
虽然外层的vector是深拷贝。
但是内层的那个vector是浅拷贝:
【C++】手把手教你模拟实现vector_第81张图片
画出来图的话,就是这样:
【C++】手把手教你模拟实现vector_第82张图片
蓝色区域的部分,在析构的时候会被释放两次。

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

【C++】手把手教你模拟实现vector_第83张图片

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

所以得要换成for循环来逐个赋值。
【C++】手把手教你模拟实现vector_第84张图片
这样的话就能实现深拷贝。

也就是这样:
【C++】手把手教你模拟实现vector_第85张图片

上面的拷贝构造用的是普通版本的拷贝构造。
我们的reserve也是有问题的。也要改成for循环的。
【C++】手把手教你模拟实现vector_第86张图片
我们其他的写法就是用了reserve,如果没有改reserve的话就会崩掉。

再看一下二维的顺序表的图:
【C++】手把手教你模拟实现vector_第87张图片

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

到此结束。。。

你可能感兴趣的:(c++,开发语言,算法,vector)