本文主要介绍vector的内容以及使用和模拟实现。
vector在英文翻译中是矢量的意思,但在c++中他的本质是一个顺序表(容器),是一个类模板,(用模板创建变量就要参考我们之前的实例化内容了)用可以改变其size的数组去实现,有了之前string的讲解,这部分的推进会顺利很多,与之前的内容分块大致相同。
从使用上来讲,vector和string的使用几乎是一模一样的,只不过string内部存放的是字符串,而vector内部可以存放任何类型,比如int char甚至是string以及vector(即vector<string>,vector<vector<int>>),而vector的各常用的函数,比如push_back,size,resize等语法与string都是相同的,我们在此就不作详细讲解了。我们在此介绍一下vector有的且string类未提到的一些知识。
std中的sort
在使用前,我们需要包含算法的头文件, algorithm。sort的用法是参数需要传迭代器(相比我们之前c中的qsort的参数,这个函数传参就简便了很多)
接下来我们实现一下vector的底层
他的结构我们可以类比顺序表,只是指向数组的指针的类型我们用模板代替
但我们查看vector的源码后发现其底层其实是使用了迭代器,因此,我们采用迭代器版本来实现底层
第一个相当于T*a,第二个相当于a+size(也是指针,指向最后一个元素的后一个的位置,相当于string的'\0'),第三个相当于capacity。接下来我们就实现它的常用函数
push_back
与之前的string一样,我们在插入之前要判断是否还有空间,如果不够就进行扩容
我们来看看扩容的具体操作,与之前的string类不同的是,我们的vector没有size、capacity的概念,所以我们需要写一个函数来实现,
这样我们就能得到我们新空间的容量大小,那么size的函数也是同理
我们重点看看这个reserve函数的实现,根据之前的string的reserve的思路,大概就是用new开空间然后memcpy拷贝然后释放旧空间然后改变指向。于是我们就可以实现以下代码。
但当我们想具体插入一些数并运行一次时发现报错了,问题就出在这个reserve函数的倒数第二行代码上。
上面是我们扩容后的新空间,但是当走到倒数第二行执行size()函数时,此时是下面的finish减去上面的start(因为start已经在上面扩容的过程被修改了)并不能得到我们想要的实际size,为了解决这个问题,我们可以进行以下修改
但是这就影响了代码的可读性,导致一个新接手的人可能不知道这里为什么是这样写,因此,我们只能另辟蹊径。其实发现,上面的方法无非就是先把保留的start给了finish然后再修改start。所以我们可以创建一个变量把修改之前的内容保存一下再用即可。
接下来我们实现一下析构函数
只需要释放原来开辟的空间以及改变指针指向即可。有了以上的各个函数以及运算符重载,我们就可以实现数组形式的[]遍历数组了,但此时用范围for是不可以的,因为范围for依赖迭代器而我们还没有具体实现底层,所以我们来实现一下迭代器然后实现一下两种形式的遍历。而迭代器的实现就简单的多了。
有了迭代器,我们就可以实现范围for的遍历了。先创建一个vector变量然后进行数据插入最后两种遍历并打印,代码如下:
当然,迭代器还有以下方法实现遍历
第一行的类型名就等同于int*it,我们上面这么写是因为iterator是定义在类域里面的,如果直接写成iterator it编译器会找不到。而我们这么写因为iterator不一定是指针,这种写法是通用的方式,而用指针替代有时候会不通过,只是我们当前可以理解为它和指针是等价的。当然,vector中的迭代器也有const版本。
由于尾删的函数没啥难度,所以我们就直接一带而过了
接下来的重点是insert和erase函数的用法,这里的参数与之前的不同,采用了迭代器,我们先展示一下它的用法
通过运行结果我们也能大致看出他们的用法了,insert在指定迭代器位置前插入数据,erase删除指定迭代器位置的元素。当然,我们如果想在指定位置插入或删除只需要在begin()后+数字就能实现了,但如果我们想在指定元素之前插入元素呢?首先我们需要在数组中找到该元素所在的位置,所以我们就要用到find函数,遗憾的是,vector内部并没有提供find函数,而是把这个函数写在了算法中,以用于复用(毕竟find函数在大部分类中还是很常用的,而string单独提供了find是因为我们无法用算法中的fing去寻找字符串中的子串)。
复用中的find函数用法如下:需传三个参数,开始查找的位置,结束的位置,需要寻找的值。如果真的找到了,就会返回目标值的位置,下面我们来实现一下在指定位置前插入指定数值。
以上就是在5的位置之前插入666.
但具体的insert和erase函数底层我们还没有实现,搞一下。思路和之前的类似,挪动之后的数据然后插入即可。
但我们真正运行的时候,发现其插入的是随机值,这里的错误涉及到一个新的领域——迭代器失效
原因是:在扩容的时候,虽然start和finish在扩容后会改变为新扩容空间的start和finish,但pos的迭代器在扩容后仍指向该位置,但这块空间已经被释放了,造成了野指针的行为。所以我们需要对pos也进行“迁移”。
但同时我们要注意这里修改的pos是形参,对实际insert的迭代器并没有修改,所以实参的迭代器在扩容操作后仍是失效的,我们就不能进行访问了,为了解决这个问题,我们统一不访问(因为你也不知道是否会扩容,扩容就变成了野指针)。 insert函数作者没有加assert,可以判断一下是否越界。
insert搞完了,看看erase
与insert同样的移动思路。但这里也存在迭代器失效的问题,在我们删除数据以后,it逐渐向后移移到最后一个元素的下一个位置,虽然其不是野指针,但我们最好不要去访问,而且,删除数据有时候还涉及到缩容的问题,那么又要开空间拷贝数据,这样it就是野指针了,更是不可访问,在前一种情况中,不同环境下的情况不同,vs下则会直接报错,而linux不会,为了统一,我们就不访问该迭代器了。但stl库中的vector也给了解决方案,库中的vector具有返回值,其返回的是删除位置的下一个位置的迭代器。
接下来我们实现一下拷贝构造函数,思路也很容易,只需要利用范围for把每个数据都进行插入操作即可。
这里我们提前进行了开空间的操作,提高了pushback函数的效率。但当我们进行拷贝实例操作时却不通过,原因是没有找到构造函数,在类与对象我们讲过,拷贝构造函数也属于构造函数,如果我们不写编译器自己生成,但现在我们已经写了拷贝构造,编译器就不会生成构造函数。在我们新创建一个vector变量时找不到其构造函数了,为了解决这一问题,我们提前剧透一下:
这行语句的作用就是强制编译器生成默认构造函数。拷贝都搞了,再看看赋值运算符重载:
这个更简单,我们利用vector中的swap函数将临时拷贝交换到我们的目标变量即可,同时我们还要把swap函数完善一下。
我们来看下一个问题,假设我们现在要在实现一个string类型的vector并插入字符串
结果运行后却是随机值,问题并不在这里,而是我们之前的扩容的函数,
memcpy本质是做到了浅拷贝,也就是说,在完成扩容后,原来的空间被delete释放掉,而新的tmp仍指向该空间,就是随机值,所以,我们采用逐一赋值的方法。
以上就是我们对vector的内容剖析以及底层实现,对小伙伴们有帮助的话麻烦点个赞再走!