vector常用的接口和底层
一.vector的构造函数
我们都是只讲常用的。
这四个都是比较常用的。
第一个简单来看就是无参构造,是通过一个无参的对象来对我们的对象进行初始化的,第一个我们常用来当无参构造来使用。
第二个我们常用的就是通过多个相同的数字来初始化一个vector。
像这样。
第三个的主要用途就是我们下图的形式。
它会通过从头遍历到结束来初始化我们的vector。
第四个毫无疑问就是通过另外一个vector对象来初始化我们的对象。
二.push_back
这个就是用来插入数据的。
就是在v1的尾部插入1和2,这些都很简单。
三.reserve
我们来探究一下它的扩容机制吧,看看和string的是否相同。
还是1.5倍的扩容机制,reserve就是预留一个空间,减少扩容次数。
剩下这些方法都和string都是相同的,大家感兴趣了可以自己尝试谢谢。
四.vector<vector<T>>
接下来我们用一个例题来讲解一下这个不好理解的知识点,同时也是非常重要 的,话不多说,我们直接上题。
想必杨辉三角这道题大家非常的熟悉,但是我们用c语言写会变得非常麻烦,我们可以看一下力扣给的c语言的函数就知道了。
-
numRows
:类型为int
,用于指定要生成的杨辉三角的行数。它明确了程序需要生成的杨辉三角规模大小 ,是后续内存分配、元素计算等操作的基础。比如传入5
,就表示要生成杨辉三角的前 5 行。 -
returnSize
:类型为int*
,是一个指针,用于返回实际生成的杨辉三角的行数。在函数内部,通常会将之前传入的numRows
值赋给这个指针所指向的内存空间 ,以此告知调用者函数实际生成了多少行杨辉三角数据。例如函数执行完,*returnSize
的值就是实际生成的行数,方便调用者后续处理返回结果。 -
returnColumnSizes
:类型为int**
,是一个二级指针。它用于返回杨辉三角每一行的列数信息。因为杨辉三角每一行的元素个数不同,通过这个二级指针,函数可以将每一行对应的列数存储起来并返回给调用者 。比如returnColumnSizes[0]
指向的内存空间存储第 0 行的列数,returnColumnSizes[1]
指向的内存空间存储第 1 行的列数,以此类推,方便调用者准确获取每一行的元素数量,正确解析返回的杨辉三角数据。
况且我们的二维数组的初始化也不支持用变量初始化,导致我们需要动态开辟内存,这是非常麻烦的。
但是如果我们使用cpp的vector<vector<T>>这些难点都会迎刃而解了。
我们先来看一下这个的原理。
我们看一下这个,需要从下往上看,一层套着一层,通过函数模板给它分配类型。
你可以理解为第一层就是指向这三个对象的指针,和二位数组很像,第一层就是通过这个vector<int>*类型的指针,来找到我们需要访问到的对象,然后再对其进行操作。
我们的这个也是可以通过[][]的形式访问的,但是和二维数组不同,静态二维数组的底层还是一维数组,
它就是通过下面的那个公式进行解引用来访问的,但是我们的vector则不是,我们的这个是通过调用两次函数来访问的,其实底层原理还是指针。
调用原理就是我们这个图中浅蓝色的代码,先通过下面的第一层找到并返回我们需要的vector对象,然后再通过这个对象再次调用[]这个运算符再返回我们所要的数据。
这就是我们c++来解决这个问题的代码了,就是先创建一个类似二维数组的一个vv对象,然后通过空参构造初始化一下,最后通过resize先把全部的数据初始化为,然后再按照正常的步骤做就行了,只需要传一个参数,更加的简单易懂。
五.insert
只讲一个用法,就是通过迭代器插入。
一个表示在头进行插入,一个表示在3的位置插入。
六.vector<char>
现在看着代码我们有一个问题,我们能否用这个vector<char>来代替string呢?
答案是肯定不能的。
vector<char>结尾是没有\0的不能兼容c语言,但是string是有\0的,可以兼容c语言。
这就是它们最大的区别。
七.reverse
这个是逆置函数,我们来看一下它的底层。
看到这个代码,我们有一个问题,我们能否把while循环中的条件改为last>first呢?
肯定是不行的,如果是顺序表,空间是连续的还可以,但是如果是链表呢,指针的比较实质上是地址大小的比较,链表的地址并不连续,后面的地址不一定比前面大,所以是不行的。
为什么是first!=--last呢?
我们看下面这个图就可以理解了。
last指向最后一个的下一个位置。
八.vector<string>
你可以理解为 底层就是一个指针指向这七个string类型的空间,类似于数组,我们用空参构造初始化了这七个,然后通过append函数来给第二个,下标为1的string对象赋值。
底层就是string* 的指针。
我们来分析一下它的范围for。
如果我们用正常的范围for,它每次都需要经过深拷贝给e,代价太大了,我们该如何解决这个问题呢?
我们可以这样,使用引用,就减少了深拷贝,提高了效率。
九.底层原理的实现
9.1 构造函数和成员变量
下面我们来实现一下。
它的底层和string是有区别的,vector的底层是通过三个迭代器来完成的。
就是这样完成的,这是三个成员变量。
下面我们来写一下构造和析构函数。
因为三个指针都指向同一块空间,所以只需要delete _start即可,构造就是把这三个成员变量全部置为空。
这样我们就完成了简单的前期工作了。
9.2 size()和capacity()
因为vector和string底层的不同,我们这里并没有直接用两个变量来存放size和capacity,而是使用了两个函数来完成,大小和空间大小的获取。
_start永远指向首位置,_finish指向_start+size()的位置,_end指向_start+capacity()的位置,所以我们可以通过指针的相减,直接获得元素的个数和空间的大小,这两个函数也是很好理解的。
9.3 reserve()
这个函数和string的一样,主要作用就是扩容的。
就是这样完成的,通过开辟一块新的空间让tmp指向它,然后再将旧空间的内容拷贝过来,最后释放旧空间指向新空间即可,我们来解释一下这个oldsize的作用,如果没有oldsize的话,因为我们的这三个指针开始均为空,地址为0x000000,当我们的_start指向新的地址之后,它的地址就会改变(假设为0x000100),此时如果我们让_finish=_start+size()的话,因为这里的_start的地址是0x000100,此时相减之后,size()就会是100,此时和我们的原来的大小大概率是不相同的,此时就会出错了。
就是这个样子。
这样子这个函数就完成了。
9.4 push_back()
这个函数就简单了,如果空间不够就扩容就行了,然后通过指针的解引用给指向的空间赋值即可,很简单的函数。
9.5 begin()和end()
这两个函数是实现范围for的必要函数,实现起来也很简单。
就是这样的,很简单。
9.6 operator[]
想要像数组一样访问元素,这个[]运算符是必不可少的。
就是这样简单的实现。
我们先来测试一下我们写的代码。
我们发现是符合我们的预期的。
9.7 pop_back
这个函数的作用就是尾删元素的,直接对_finish--就可以了,让访问不到就行,不用把空间释放了。
9.8 insert
这个是通过迭代器版本的插入,这个用的是比较多的,想在哪里插入,直接让迭代器加上距离就行了,我们来看一下下面的代码。
我们发现此时出现了问题,我们把它称为迭代器的失效,我们来研究一下为什么会失效呢?
我们在插入四个数据之后,此时通过insert插入的第五个数据,此时就要扩容了,应该是这里出现了问题,我们来分析一下。
这是扩容前的场景,我们再来看一下扩容之后的情景吧。
扩容之后,你的it还是指向原来的空间,但是你的_start还是指向原来的旧空间,此时_finish和it就不在同一块空间内,这里应该是it的空间大于end指向的空间地址,因此没有进入insert函数的while循环当中,直接++_finish了,此时第五个数就是随机数了。
这就是迭代器失效的一种情况。
修正方案:
我们这样修改就不会出现这种情况了,我们让it也指向了它在新空间内的位置,此时就没问题了。
再来看一下这个情况,还是在扩容的时候插入,此时我们是通过一个迭代器表示我们要插入的位置,然后我们运行一下看看。
思考一下,此时我们的*it为什么会是随机数呢?
这是因为,insert的第一个参数是传值传参,传入的是副本,此时那个副本进入了新的空间,释放了旧空间,此时这个it还是在原来的地方指着没有变,所以就出现了这个情况,空间被释放出现了野指针。
就是这样的,it指向的空间被释放了,所以是野指针,那么有人就说了,我们可以通过传引用来完成这个操作啊。
此时我们无法传入一个常量内容了,也不方便,所以,你只需要记住,这里的it是野指针就行。
9.9 erase
这个就是我们的erase函数,这个函数的作用就是删除指定位置的元素,我们通过找到pos+1位置的元素向前覆盖就可以完成了。
我们完成了这个函数,此时我们来做一个题
删除所有的偶数
这就是我们的代码实现了,我们看上去没有错误,但是我们来运行一下。
我们发现程序崩溃了,这是什么原因呢?
我们来分析一下。
我们发现了两个问题,第一个就是我们没有检查3这个数字是否为偶数就直接去4那里了,但是这也不是崩溃的原因,原因就是
在第二个assert处程序崩溃了。
开始的时候_finish是在4的后面那个位置的,但是it到达2处,此时调用erase,此时2被3覆盖,3被4覆盖,4的位置的值不动,此时_finish在第二个4的那个位置,此时it直接去到第一个4那里,继续走erase函数,此时 ,it就到第二个4的位置了,此时_finish在3的位置,然后it永远不会和_finish相等的,况且现在it还大于_finish,传入参数it就是pos,pos>_finish,所以断言触发了。
我们现在就来解决这两个问题吧。
这个代码的逻辑就很好解决了那两个问题,但是vs下还是跑不了,但是g++下能跑,这就是vs的一种机制了,对于迭代器的失效检查非常严格。
vs认为,你的迭代器insert和erase之后就会失效,会改变标记,导致不让用。
我们该怎么办呢?
只需要加一个返回值就行了
我们只需要把这个位置返回给it迭代器就会使它不失效了,每次更新一下it迭代器就可以使它不失效了。
9.10 resize
两个参数,第二个参数给了一个缺省值,这里相当于给int和其他类型也升级了一个构造函数,如果T=int,那么T()就是0,这个函数实现起来也不难,和string的基本一样。
分别在不同区间要干的事情。
9.11 拷贝构造
我们直接先开了一个空间,然后通过push_back赋值即可。
vector(const vector& a);//这样也是ok的,在类里面可以直接写vector但是类外面不行,在类里面用vector的时候可以不写后面的泛型,这个看见了知道就行,尽量不要使用。
9.12 operator=
这个是我们正常的实现流程,但是我们受到前面string中实现的启发,我们有更简便的方法来实现。
反正这个函数中的形参vector<T> a是没有用的,直接交换一下空间的指向,通过析构也可以很好的释放旧空间,很方便。
结合着上面的三个方法我们来写个代码。
我们发现程序有崩溃了,这是怎么回事呢?
这个主要的崩溃的点就在于vv=v2这条语句,执行这条语句就会进入到拷贝构造当中,因为我们要传值传参,就会调用拷贝构造生成一个副本,在生成这个副本的过程中,就出现了问题,我们来看一下。
此时就会进入到reserve函数中。
因为当我们的v2传给赋值运算符形参的时候,会调用拷贝构造,当调用拷贝构造的时候,进入了reserve函数,此时这里的形参封装的三个迭代器参数都是随机值,你此时调用capacity的时候就出现了问题,所以我们得初始化一下,因为我们的拷贝构造没有给初始化列表,所以形参的三个迭代器就是随机值。
解决方案一:
解决方案二:
给缺省值,我更推荐这个方案,好处就是给了缺省值,不管是调用拷贝构造还是构造函数,都会通过给的缺省值走初始化列表,这样就不会导致随机值的情况了。
这样问题就得到完美的解决了。
9.13 vector<string>
我们看一下这个代码,打印四个不牵扯扩容的时候是没有任何问题的,但是打印第五个的时候出现了问题,我们来看一下。
出现了乱码,我们来解决一下这个问题,其实这个问题不是太容易发现,我们肯定知道是扩容的时候出现的问题,我们来看一下。
程序崩溃的主要原因在于自定义 vector
类的 reserve
方法中使用了 memcpy
来复制对象。memcpy
是一个按字节复制的函数,它只复制对象的二进制内容,不会调用对象的复制构造函数。当 T
是像 std::string
这样的复杂类型时,使用 memcpy
复制会导致浅拷贝问题,使得多个对象共享同一块内存,在析构时会造成重复释放内存,从而引发程序崩溃。
导致我们两个空间内的前四条数据指向的是同一块内容,当我们的上面那一块空间被delete了,这是下面的前四条数据也会被delete,此时就出现了乱码的情况,第五个不受影响。
我们该怎么解决这个问题呢?
这样就可以了。
当是string的时候这就是通过string的赋值运算符=来完成的,这个运算符返回的是string的值,而不是其他东西,其他的类型也可以通过这个来赋值。
9.14 再探构造
std库里面有一个这样初始化的形式,接下来我们也来实现一下吧。
它就是通过一个initializer_list<T>参数来完成的,你可以把它理解为开创了一个数组,两个指针,一个指向头,一个指向尾部,它和数组很相似,都是在栈上开辟了空间我们可以看一下。
发现它们两个的地址很相似,这个不必深究,会用就行。
这个构造的实现就是模板套模板的形式,模板类里面再弄一个模板函数,我们下面来看一下它的使用。
a就是上面定义的数组,我们可以看到功能非常的强大,可以使用迭代器,也可以存入一个字符串类型的,也可以用数组初始化,功能很强大。
接下来就是最后一个了。
就是用100个0初始化那样的形式。
就是这样实现的很简单,接下来我们来用一下。
这个结果没问题,我们再把下面的注释拿掉再来看一下。
我们发现报错了,怎么回事呢?
我们发现当我们使用两个int类型的时候,它会调用到上面的我们的那个模板,因为没有非常匹配的,所以编译器就选择了模板。
一个调用上面的,一个调用下面的,你对int类型的数解引用,所以就报错了,我们该如何解决呢?
有人可能会把size_t改成int,但是改成int之后,我们要是再想使用size_t类型的呢,它还是会匹配到上面,索性两个都留下就没问题了。
此时就没有问题了。
到这里vector就结束了。
十.结束语
感谢大家的查看,希望可以帮助到大家,做的不是太好还请见谅,其中有什么不懂的可以留言询问,我都会一一回答。 感谢大家的一键三连。