vector相关功能的底层实现

  如果是经常编写C++代码的同学,相信大家肯定对于vector都不陌生。但是对于vector的底层逻辑大家真的熟悉吗?在本次的博客当中我们将会向大家介绍关于vector底层的逻辑,以及在实现底层逻辑需要注意的事项。

  查阅相关的文档我们会发现,和我们之前编写的string有所不同。vector采用的是三个指针的形式进行表示的vector内存的大小以及容量的。

  就像上图中所显示的那样,其中的start  /  finish  /  end_of_sotrage 才是vector类当中的私有变量。其中finish和start之间限定的是vector当中已经存储的数据的个数,end_of_storage和start当中限定的是当前vector对象所拥有的容量。其结构大致如下:

  那么依据官方文档的设计,我们自己来实现一个和官方文档功能类似的vector类。

  vector的大致框架

  根据上面看到的那样我们可以实现一个vector的大致框架。我们可以设置三个iterator。其中的iterator和我们之前实现的string相同。使用指针重命名即可。需要我们注意的是,由于vector允许我们自主设置其中存储元素的类型,因此我们需要使用函数模板进行实现。之后实现一个无参的构造函数,用于初始化vector对象的指针即可。

push_back相关功能的实现

  仅仅实现了一个框架无法对我们编写的代码进行验证。所以最简单的就是再编写一个简单的插入函数,来测试我们自己的vector是否可以正常运行。

  一谈到插入函数我们想到的就是容量的问题。对于我们插入数据来说最重要的就是对容量的检查。所以我们在实现插入函数之前需要实现的是reserve函数。

  reserve函数的实现

  我们需要先判断想要开辟的空间的大小是否大于原数组的大小,如果小于原数组的大小,我们不需要进行扩容直接返回即可。之后只需要开辟一个新的空间,将原vector当中保存的数据拷贝到新的空间当中即可。

  那么我们的扩容代码就简单的“完成了”。

  那么经过扩容操作之后,我们直接将数据赋值到finish的位置上面即可,实现的push_back函数如下:

  当然其中的capacity()函数仅仅是返回了endofstorage和strart相减的结果,size()返回的是finish和start相减的结果,在这里就不再赘述,直接展示实现后的结果即可。

  最后为了方便打印我们vector当中的数据,我们还要实现begin()和end()函数。当然这些都很简单。

  经过一系列操作之后我们的push_back函数也就编写结束了,接下来就进行测试操作。

  但是我们会发现我们编写的代码崩溃了,崩溃的原因是什么呢?我们一起来分析一下。

  崩溃的原因是我们的finish是一个空指针,但是我们命名已经进行扩容操作了呀,这说明我们的reserve函数编写的有问题。我们进入reserve函数当中进行具体的查找原因。

  进入reserve函数当中调试检查之后我们发现,endofstorage显示正常但是finish和size()的值都出现了问题。

  这是因为我们新开辟的空间的问题。还记得我们的size()函数是怎么实现的吗?是使用_finish-_start完成的。

  但是我们在这一行就已经将start修改了,那么finish和start表示的就是两段不同的地址的开始和结束,所以才造成了我们代码的问题。想要修改这个bug我们只需要提前记录数组当中的元素个数,之后的finish使用该数据进行修改即可。

  重新运行我们的代码发现代码一切“正常”。

  insert插入函数

  和push_back函数相对应的插入函数就是insert函数了。那么接下来我们就来实现insert插入函数的相关功能。

  只要向数组当中插入数据我们需要想到的就是先检查是否需要进行扩容操作。经过扩容操作之后,根据vector所特有的性质,我们需要先将插入位置之后的数据全部向右移动一段距离之后才能进行数据的插入。

  但是运行代码之后依旧会产生程序崩溃。

  仔细阅读代码之后我们会发现其实我们发生了和之前一样的错误,就是经过扩容之后依旧使用扩容之前的地址所产生的错误。

  那么想要修改这个错误,同样的我们只需要记录相对地址即可。

  运行程序一切“正常”。

    erase函数

  和插入相对应的就是删除操作了,接下来我们就来实现vector的删除操作。

  对于删除操作的函数,我们需要注意的是:不能直接使用已经删除之后的指针,我们需要使用erase的返回值。接受之后才可以使用。因为如果我们删除的是最后一个元素的话,那么我们的finish在向前移动一个位置之后,我们的pos位置就是一个非法的地址,在使用就会出现错误。同时在一些平台上面还会不接受直接使用删除过后的迭代器地址,如果直接使用就会直接产生报错,严重影响了平台的兼容性的问题。因此我们应该合理规范的使用erase函数和insert函数。

  resize函数

  之后需要实现的就是我们的resize函数了。对于resize函数的实现,只有一个新奇的使用方式而已。据我们所知,在使用resize函数的时候,我们可以在调整大小的同时,还可以进行初始化操作。那么就会产生一定的分歧。对于自定义类型的数据和内置类型的数据怎么同时进行区分并初始化呢?

  在C++当中,系统将我们的内置类型的数据进行升级,产生一种新的类似于调用构造函数赋值方式。如下图所示:

  因此我们可以创建一个默认参数为T()的形式,如果是我们的自定义类型,如果不主动传参就会自动调用默认构造,对于内置类型的数据同样适用。那么resize函数的实现就如下图所示:

  需要注意的是:我们在使用默认参数的时候需要使用const进行修饰,因为如果我们想要传入的是内置类型的话就会产生一个权限放大的问题,造成权限不匹配的报错。

拷贝构造和赋值运算符重载

  之后我们来实现函数的拷贝构造和赋值运算符重载的操作。对于拷贝构造就像是我们之前说到的那样。在实现拷贝构造的时候参数需要使用&的形式,不然就会产生无穷递归的情况。

  运行一下程序:

  看似一切正常,但是真的是这样吗?其实不然,相信大家在使用vector的时候肯定也使用过在vector当中嵌套其他类型的数据吧?我们可以尝试一下。

  

  我们会发现程序又崩溃了,我们继续来分析一下原因。

  

  我们会发现程序运行到这一行的时候产生了报错。认真思考一下就会明白其中的道理:当我们调用memcpy函数进行拷贝操作的时候,我们进行的只是简单的浅拷贝操作。但是对于string这样的自定义类型的来说,如果需要自主开辟空间的话,浅拷贝就会产生很多不必要的影响,比如说:在释放程序的时候就会由于重复释放造成程序崩溃。这就是我们此处程序崩溃的原因。

  因此对于具有模板的类来说,如果我们想要进行容器当中数据的拷贝操作,最好手动进行。因为如果我们手动进行赋值操作的话,系统就会默认匹配到类的赋值运算符重载。对于赋值运算符重载来说我们会手动开辟一个新的空间,这样的话我们再释放的时候就不会出现同一块空间重复释放的情况产生。将代码修改成为如下形式所示:

  同样的,我们的拷贝构造当中也需要自己手动进行拷贝才不会出现错误。

  程序运行结果如下:

  之后实现我们的赋值运算符重载操作:

  和我们之前编写过的string的赋值运算符重载相同,我们可以手动进行开辟空间赋值,也可以采用现代写法进行赋值。基本原理就是系统在传参的时候自动调用拷贝构造。之后创建出一个新的对象,我们将现有的数据内容和已经创建完毕的内容进行交换。等到一切准备完毕之后,新创建的对象的信息就被我们利用,老的信息就被新创建的对象自动销毁了。

  当然这种写法的前提就是我们需要有拷贝构造函数,以及析构函数。补充我们的析构函数:

  重新运行我们的程序:

  依旧是一切正常。

   [ ] 运算符重载

  之后我们想要实现的就是 [ ] 运算符重载了。因为vector作为顺序表来说最大的特点之一就是可以随机进行访问。因此 [ ] 运算符重载函数必不可少。

  

  其实这个函数极为简单,我们需要注意的就是对下标位置的检查,防止非法位置数据的访问即可。

  运行程序一切正常:

  说到 [ ] 的使用我们就会想到,我们不仅可以对普通的内容进行访问,还可以对const修饰的对象进行访问操作,只不过不能进行修改而已。那么我们的程序可以实现这个功能吗?

  很明显我们的 [ ] 可以进行修改,如果想要对const对象使用不能进行修改那么就需要我们重新生成一个const修饰的返回值的版本了。

  同样的对我们的其他版本的内容补充上const版本。

  const迭代器

  当我们的vector是cosnt进行修饰的时候我们不能对其中的数据进行修改了。并且如果没有cosnt修饰的对象的时候就会产生报错,示例如下;

  因此我们才需要对上述的函数实现一份具有const修饰的内容。

  多参数构造和iterator构造

  最后我们来实现多参数构造函数以及iterator函数构造。

  这一部分同样很简单,我们只需要先申请足够的空间之后直接赋予我们指定的位置数据即可。

  功能一切正常。

  之后就是我们的iterator构造。

  需要我们注意的是:在使用迭代器的时候我们不能直接使用我们在类当中重命名的迭代器,而是应该使用一个迭代器模板。因为我们在使用vector的时候会使用多种形式的迭代器初始化我们的目标对象,因此使用模板就会自主根据我们传入的迭代器进行适应,更好的为我们的使用提供服务。

  但是我们会发现,如果我们编写完成iterator构造之后就会出现新的错误。但是我们在测试的时候并没有对iterator进行测试呀?

  这种错误产生的原因是因为:我们系统在检验数据的时候会优先进行最适应的数据进行匹配。因为我们的多参数的构造函数参数为size_t 和 T 在使用的时候会产生两次转换。首先系统会将我们的数据强转成为size_t类型的数据之后实例化T。但是如果存在迭代器构造的时候,系统只需要进行一次转换,直接将类型实例化成为T即可。所以我们的系统优先走迭代器函数。但是我们在其中的解引用操作就会产生非法的内存访问的问题。这才造成了我们的报错。

  更正这个错误我们只需要添加一个更加适合的int类型的函数数据作为参数即可。如下:

  程序正常运行。最后测试我们的迭代器构造函数是否可以正常使用。

  • 12
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿白逆袭记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值