C++笔记-vector
一.vector的基本使用
1.1vector的基本概念
在 C++ 里,std::vector是标准模板库(STL)提供的一个动态数组容器。它能存储同一类型的多个元素,而且可以在运行时动态改变大小。
其实vector就是一个顺序表,而顺序表我们之前都学习过,所以vector和string底层都为数组,所以两者在库中的一些接口等都非常相似:
可以看出vector的接口很多都是我在string类中讲过的,并且因为两者底层都为数组,所以在使用上也是基本一样的。
1.2vector的基本使用
因为和string高度相似,所以我就演示一下vector的基本用法,主要是将下面的实现vector,来帮助我们更好地理解vector。
尾插和遍历数组都和string是一样的,唯一的区别是我们在创建vector对象时要加上<>,里面标明数组中的数据是什么类型。
迭代器和范围for来遍历数组也是和string一样的方式。
二.vector的实现
2.1vector框架的实现
和之前的string一样,首先把vector的框架写出来。
和之前string不一样的是,这里vector我们要用到模板,因为底层vector就是用模板那来实现的。
另外成员变量也能也有人不理解,这里这样定义成员变量的名字和成员变量的类型是因为vector底层源代码是这样定义的,源代码大家有机会可以看看,我们既然实现vector,就尽量跟着底层走。
我们知道vector本质是一个顺序表,所以_start代表的就是首元素地址,_finsh代表的就是最后一个元素的下一个位置,_end_of_storage代表的就是容量。
因为它们都是指针,所以构造和析构实现时都把它们置为nullptr,另外把begin和end这两个迭代器实现了,后面也会使用。
注意:这次vector中各种接口的声明和实现都要在类内实现,因为我们使用了模板,模板如果分开声明和实现会出现链接错误。
2.2cpapcity的实现
这里直接用_end_of_storage - _start就可以直接得到此时数组容量的大小,指针相减得到就是两个之间的大小。
2.3size的实现
同样的方式,用_finsh - _start就可以得到数组中数据的数量。
2.4reserve的实现
在实现reserve中,如果此时_start为空,就可以直接让_start接收tmp的位置。
这里面最大的问题就是计算_finsh的位置,如果不提前计算此时的size,那么后面扩容后,_start已经转移到新的地址,而此时我们调用size来计算_finsh时,就会出问题。
因为此时_start在新的空间,而_finsh在旧的空间,如何计算size?
直接相减肯定会出问题,所以要提前计算size的大小,就是为了避免这种问题。
剩下的就是通过_start来找到_finsh和_end_of_storage的位置即可。
2.5push_back的实现
逻辑和之前实现string中的push_back一样,在尾插前检查此时数组是否满了,满了就扩容,最后在_finsh处加上目标值,并令_finsh++即可。
2.6pop_back的实现
这个实现起来就很简单,不需要将最后一个值置为0,因为如果走后一个值本来就是0呢?
所以_finsh--即可。
2.7[]符号的实现
实现起来也比较容易,直接返回相应下标的值即可。
2.8insert的实现
insert大体逻辑和之前string中之一样的,不一样的是在string中我们用下标来遍历数组,但是会出现头插时类型提升的问题,这次我们使用指针来实现,可以解决之前的问题。
但用指针又出现了新的问题,就是图片中所说的迭代器失效的问题,解决问题的关键就是pos,上面的代码是解决问题后的代码。
我们接下来看看如果不做处理,迭代器失效会出什么问题:
注意:要检查本身insert中代码实现是没问题的。
此时就出现问题了,insert本身的代码是没问题的,我们再看另一个现象:
此时又没问题了,我们通过刨析代码发现,出现这两种情况是因为第一个扩容了,而第二个扩容了,第二个例子没问题说明reserve本身是没问题的,那么就是因为扩容才导致了这种问题。
其实这个问题和上面reserve遇到的问题很相似:
用图来解释就是我们扩容后,_start已经来到了新的空间,而pos依旧指向旧空间,那么在通过it和pos位置遍历数组是不是就会出问题?
这就是迭代器失效,在这里本质上就是野指针的问题。
而为什么第二个例子没有问题的呢?
因为第二个例子在尾插前就已经扩容过了,此时空间没有满,用的还是原来的空间,所以不会出问题,所以为了解决第一个例子出现的迭代器失效的问题,我们在扩容前就记录下pos和_start之间的距离,扩容后更新pos的位置,这样就能解决问题。
我们再来看一个问题:那尾插后我要访问pos位置处的值呢?
已经解决了扩容后导致的问题,为什么又不能访问pos的值了呢?明明已经更新了pos的位置。
因为我们实现的是传值调用,如果用引用就可以解决这个问题,但是此时又会出现新的问题:
我们在这种使用时就会出问题,因为此时的pos是临时对象,而临时对象具有常性,也就是之前讲的权限扩大的问题,可能又会与人说加个const就行了,那又会出现新的问题,这里我就不过多赘述了,而底层用的也是传值调用,所以就按这个来。
所以说呢insert之后就默认pos已经失效了,不能再使用pos了,因为并不清楚底层什么时候会扩容。
2.9构造函数的其他实现方式
这两种方式都是对数组进行初始化,第一种方式是初始化为n个一样的值,第二种方式是直接初始化为任意个一样或不一样的值。
第一种方式中的参数利用了匿名对象,因为要给缺省值,但是不能直接给0,因为数组中不一定就是整型,所以利用匿名对象就可以解决这个问题。
第二种方式的参数有的人会看不懂,这种方式其实c++11中引入的一种方式,它是std标准库中的,它里面自带begin,end和size。
两种构造函数的基本用法就如上图所示。
2.10erase的实现
这是正常情况下我们要实现的erase函数,但是这种方式会出问题:
这里我们先用系统给的vector来实现这段代码,发现出现了错误,但是我们检查这段代码的逻辑是没有问题的,会出现这个错误的原因就是和上面一样,都是迭代器失效的问题。
因为在vs编译器下,如果调用了erase函数,会默认当前的迭代器已经失效,然后会强制检查迭代器,所以我们对vv++就会出问题。
而在其它编译器下不一定会有这种问题,就比如g++编译器下,就不会强制检查,所以相同的代码下就不会报错。
所以我们要怎样解决这种问题呢?
其实只需要更新迭代器的位置即可,使它是有效的,具体过程如下:
将erase函数返回的位置赋值给vv即可,其实vv本身的位置没有改变,只不过将它原本的值重新赋值给自己而已,使其重新变为有效的迭代器,所以我们上面写的erase函数就不太合适,应该写成如下这样:
其实去看vector底层实现的erase接口,也是这样实现的,是有返回值的,不是我们之前实现的void。
2.11resize的实现
vector中resize的实现比较string就会更容易些,就分为两种情况,大于capacity和小于等于capacity,大于capacity是扩容+插入数据即可,小于等于时,更新_finsh的位置即可。
即使感觉如果n>size && n<capacity这个范围怎么办,其实缺省值就把这个问题解决了,超出的部分都初始化为0。
2.12=符号的实现
=符号的实现需要用到swap函数,这样实现起来其更为简单。
swap:我们之前的传统逻辑是再创建一块空间,把其中一个数据先拷贝到临时空间,再进行交换。而现在使用的现代写法直接交换两个指针,不需要再创建新的空间。
可能有人会疑惑为什么还要交换_finsh和_end_of_storage,如果不交换这两者的话,那么交换后的_start中的_finsh和_end_of_storage是不对应的,这么做就会使两者的意义就没有了,所以两者也要交换。
注意:要使用的是std标准库中的swap函数,std库中的swap函数是一个模板,string和vector中的swap接口都是基于std标准库中的swap函数来实现的。
完成swap函数再实现=符号就简单许多,直接交换即可,最后返回*this。
注意:这里不能使用引用,因为我们实现的是=符号,不能改变等号右边的值,所以要使用传值调用。
最后再来看一种现象:
我们不再使用int类型,使用string类型,此时我们在vector插入几段字符串没有问题把,我们再看:
此时就会出问题了,但问题出在哪儿了呢?
很明显,问题出在扩容这步操作了,但是扩容我们之前都已经试验过了,没有问题,我们来调试看看:
我们发现在经过delete这段代码后,就出现这种现象了,那说明delete这部操作有问题,但是_start指向的就是一个数组,这样delete没有问题啊。
是的,怎么看这段代码都没有问题,那么是哪里出的问题呢?
这里我直接就说答案了,是memcpy出的问题导致的这种现象,我们通过画图来演绎出memcpy的过程:
把图画出来后相信大家一眼就看出问题出在哪儿了,这就是使用memcpy的弊端,当vector中是内置类型时,这么写是没有问题的,但是对于自定义类型这么些就不行了。
所以要怎样解决这种问题呢?,很简单:
直接利用循环一个个复制过去即可,有人会疑惑为什么这样就可以解决了呢?
拿string举例,当赋值的时候,就会调取string中的拷贝构造函数,而string底层中的拷贝构造函数是深拷贝,所以就解决了这种问题,内置类型就更可以实现了。
以上就是vector的内容。