当前位置: 首页 > news >正文

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就结束了。

        

十.结束语

        

         感谢大家的查看,希望可以帮助到大家,做的不是太好还请见谅,其中有什么不懂的可以留言询问,我都会一一回答。  感谢大家的一键三连。

       

相关文章:

  • AI对话高阶玩法:解锁模型潜能的实用案例教程
  • 消息中间件面试题
  • 开源TTS项目GPT-SoVITS,支持跨语言合成、支持多语言~
  • java面向对象06:封装
  • cmd 终端输出乱码问题 |Visual Studio 控制台输出中文乱码解决
  • Day08【基于预训练模型分词器实现交互型文本匹配】
  • 考研数据结构之树与二叉树的应用:哈夫曼树、哈夫曼编码与并查集
  • JavaWeb开发 Servlet底层 从概念到HTTP请求 到web服务器再到servlet
  • ROS2 常用
  • How to run ERSEM
  • linux上安装vimplus 从零开始
  • 使用Python构建桌面图片浏览器
  • cursor如何回退一键回退多个文件的修改
  • Docker 安装 Elasticsearch 8.x
  • Java二叉树深度解析:结构、算法与应用实践指南
  • 【教程】检查RDMA网卡状态和测试带宽 | 附测试脚本
  • Java公平锁和非公平锁实现原理
  • 图论-BFS搜索图/树-最短路径问题的解决
  • 2025 cs144 Lab Checkpoint 2 小白超详细版
  • python 安装win32com.client库
  • 为什么要研制大型水陆两栖飞机?AG600总设计师给出答案
  • 女子伸腿阻止列车关门等待同行人员,被深圳铁路警方行政拘留
  • 上海这台人形机器人完成半马:无故障、无摔倒,冲过终点不忘挥手致意
  • 马上评|机器人马拉松,也是具身智能产业的加速跑
  • 遭遇FIFA三个转会窗禁令,申花有苦难言将选择赔偿
  • 嵩山少林风景区女游客进男厕:不能止步于批评