【C++学习】vector的使用及模拟实现

作者:一只大喵咪1201
专栏:《C++学习》
格言:你只管努力,剩下的交给时间!
【C++学习】vector的使用及模拟实现_第1张图片

vector的使用及模拟实现

  • 构造函数
    • 模拟实现
    • vector的扩容机制
    • 模板参数推演
  • vector与容量有关的接口
  • vector的常用接口
      • find
  • 迭代器失效问题
  • 更深层次的深拷贝
  • 总结

在学习了string以后,我们对模板有了一定的了解,下面本喵来给大家介绍一下STL模板中的vector。

vector其实就是顺序表,它是在管理数组,并且它是一个类模板,可以实例化为不同类型的类,来供我们使用。STL标志模板库给我们提供了很多的成语函数接口来供我们使用,使我们编程的效率大大提高。

本喵在介绍它使用的同时,也会讲解它的底层原理,来模拟实现它,好让我们对vector有一个更深的了解。

构造函数

【C++学习】vector的使用及模拟实现_第2张图片
官方库中提供上图所示的几种重载的构造函数,根据它们的函数声明就可以知道它们是如何使用的。

【C++学习】vector的使用及模拟实现_第3张图片
同样地,还提供了push_back函数来向vector中插入数据。
图

其中,形成类型const value_type&就是T&,T是模板参数,在实例化的时候可以是内置类型,也可以是自定义类型。

【C++学习】vector的使用及模拟实现_第4张图片
上图代码,向vector中插入数字,并且通过范围for打印出来。

  • vector后的<>中的内容就是模板参数,可以是内置类型,如int,char,float等等
  • 也可以是自定义类型,比如Date,甚至是vector等自定义类型。

模拟实现

vector的成员变量不和string的一样,并不是_size,_capacity_等,而是三个指针。

本喵来给大家看一下SJI版本的STL源码:

【C++学习】vector的使用及模拟实现_第5张图片
首先可以看到,vector是一个模板类,typedef的一些类型也是我们后面经常会用到的。

【C++学习】vector的使用及模拟实现_第6张图片
可以看到,它的成员变量只有三个,而是三个迭代器。

【C++学习】vector的使用及模拟实现_第7张图片
在模拟实现的时,将我们自己模拟的vector放在自己的命名空间wxf中,图中红色框中的内容是没有参数的默认构造函数。

【C++学习】vector的使用及模拟实现_第8张图片
为了向模拟实现的vector中插入数据,需要我们自己实现push_back()函数,如上图中所示。

  • 红色框中是为了扩容而进行的三目运算。
  • 当vector刚刚创建时,它里面是没有任何内容的,此时它的容量是0,此时需要给一个初始容量,这里本喵将其设置为4.
  • 当vector中的容量不够,并且不是0时,进行二倍扩容,将容量变成原来的二倍。

图
还需要实现上图所示的俩个函数来辅助。

【C++学习】vector的使用及模拟实现_第9张图片
为了能够使用reserve函数来扩容,需要我们自己来模拟实现reserve函数,如上图所示。

  • 红色框中,需要提前记录一下当前vector的size。
  • 否则扩容后的_finish就会是0,因为temp加的值会是size(),而这个size是此时求出来使用的,由于_start已经发生了变化,所以求出来的值是一个和_start大小相等,符合相反的数。

可以自己去尝试一下,看看发生什么样的错误。

【C++学习】vector的使用及模拟实现_第10张图片
此时就初步实现了vector和push_back。

vector的扩容机制

vector个string一样,也是动态变化的数据结构,所以就会存在扩容,下面本喵来给大家看看vector在不同平台下的扩容机制。

vs2019平台:

【C++学习】vector的使用及模拟实现_第11张图片
向vector中插入100个数据,当size和capacity相同的时候打印当前容量,因为此时会发生扩容,可以看到,每次扩大到之前容量的1.5倍左右。

【C++学习】vector的使用及模拟实现_第12张图片
同样的代码,在g++编译器下就是按照2倍来扩容的。

本喵这里的扩容采用的是g++的机制,也就是严格按照之前容量的2倍来进行扩容。

【C++学习】vector的使用及模拟实现_第13张图片
除了没有参数的默认构造函数,还有使用迭代器区间来初始化类对象的构造函数。如上图所示,v2成功的用迭代器区间进行构造。

下面本喵来模拟实现一下它:

【C++学习】vector的使用及模拟实现_第14张图片
在模拟之前,需要实现上图中的俩个接口。

【C++学习】vector的使用及模拟实现_第15张图片
在类模板中,只要有需要是可以继续套模板的,如上图中的红色框中内容。

  • 这里使用模板的原因是,为了通过迭代器区间来实现,至于迭代器的类型并没有固定为指针。
  • 在vector和string中,迭代器的本质就是指针,但是在列表等其他数据结构中,迭代器的本质就不是指针了,所以这里使用的是泛型编程。

模板参数推演

【C++学习】vector的使用及模拟实现_第16张图片
上图模拟实现的是使用n个T来构造类对象的构造函数。

  • 红色框中给是一个缺省值,该缺省值是T的匿名对象
  • 如果是内置类型,比如int,它同样有默认构造函数,初始化后该int类型变量的值为0.
  • 如果是自定义类型,在创建匿名对象的时候会调用它的默认构造函数。

【C++学习】vector的使用及模拟实现_第17张图片
执行上图中的代码:

【C++学习】vector的使用及模拟实现_第18张图片
在编译的时候,报了一个非法间接寻址的编译错误。

【C++学习】vector的使用及模拟实现_第19张图片
但是使用char来实例化vector,并且使用该构造方式创建对象的时候,就不再报错,而且创建成功了,这是什么原因呢?

【C++学习】vector的使用及模拟实现_第20张图片

  • 当使用int将vector实例化以后,构造函数的模板产生T就成了int类型。
  • 在外部将int类型的5传给构造函数以后,由于第一个形参是size_t类型的,所以需要发生整型提升。
  • 但是编译器此时认为,传给构造函数的俩个参数都是int类型,而此时构造函数的俩个形参一个是size_t,一个是int类型,要想匹配还需要整型提升第一个参数,比较麻烦。
  • 编译器是比较懒的,不想多干活,所以它发现,下面的使用迭代器区间的构造函数,可以将模板参数推演为int类型,以此来供传过来的俩个int类型使用。
  • 所以在函数内,对int类型解引用就发生了错误的间接寻址错误。

那么为什么,使用char类型来实例化vector就不会发生这个错误呢?

  • 因为此时构造函数中的T被指定成了char类型,而另一个传过来的实参是int类型。
  • 一个int类型,一个char类型,编译器为了少干活,就没有使用迭代器区间的构造函数,也就是没有推演模板参数,而是采用了将第一个参数整型提升为size_t的构造函数。

编译器也是懒狗,它会寻找工作量最少的方式来实现用户的要求,也就是会根据数据类型自行决定是推演模板参数类型,还是使用已有的函数。

【C++学习】vector的使用及模拟实现_第21张图片
解决这个问题的办法也是很简单,只需要重载一个int类型的构造函数即可,如上图红色框所示,此时模板参数实例化为int类型也不再报错。

拷贝构造函数:

构造函数学习了以后,按照成员函数类型,还需要有拷贝构造函数。

【C++学习】vector的使用及模拟实现_第22张图片
STL库中的拷贝构造函数声明如上,它的形参是一个vector的引用。

【C++学习】vector的使用及模拟实现_第23张图片
如上图所示,使用v1来构造v2,可以看到v2中的内容和v1一模一样。

模拟实现拷贝构造函数:

【C++学习】vector的使用及模拟实现_第24张图片
上图所示的是拷贝构造函数的现代写法,也就是抓壮丁,在string的模拟实现时,本喵详细讲解过。

这里使用的swap函数不是标准库中的,所以需要我们自己实现:

【C++学习】vector的使用及模拟实现_第25张图片

  • 拷贝构造函数中的交换函数,之所以不使用标准库中的sawp,是为了减少系统的开销。
  • 如果模板参数T是一个自定义类型的时候,使用库中的swap代价就会非常大,因为自定义类型在这个过程中会发生拷贝。
  • 在模拟实现的swap中再使用库中的swap时,仅仅是指针变量直接的交换,发生拷贝也就4个字节大小,代价并不大。

【C++学习】vector的使用及模拟实现_第26张图片
必须先实现俩个const迭代器的成语函数,如上图所示,因为拷贝构造的形参x是const类型的vector,此时它的this指针是被const修饰的,它的成语函数begin和end得到的迭代器也必须是被const修饰的,否则就会发生权限的放大,是不被允许的。

【C++学习】vector的使用及模拟实现_第27张图片
顺带着再实现一下析构函数,非常简单,本喵就不作讲解了。

赋值运算符重载函数:

【C++学习】vector的使用及模拟实现_第28张图片
赋值运算符重载函数只有一个,并没有多个重载类型。

【C++学习】vector的使用及模拟实现_第29张图片
如上图所示,成功的将v1赋值给了v2,其实这样看来,赋值运算也是属于构造的一种。

赋值运算符重载的模拟实现:

【C++学习】vector的使用及模拟实现_第30张图片
这里的形参不能使用引用,否则会将赋值的对象改变。这里会发生拷贝构造,创建出一个新的对象v,但是这个v不在栈区上,而是在堆区上,将this指针指向的内容和v进行交换。

vector与容量有关的接口

【C++学习】vector的使用及模拟实现_第31张图片
上图中的接口全部都是和容量有关的,如size,capacity,reserve接口在前面介绍构造函数的时候已经介绍了,下面本喵来介绍一下其他没有介绍的。

resize:

【C++学习】vector的使用及模拟实现_第32张图片

vector的resize和string的resize是一样的。

  • size < n < capacity:仅调整size,也就是只改变_finish的值。
  • n > capacity:size和capacity都发生了改变,也就是发生了扩容
  • n < size:仅调整size,只改变_finish。

原则: 缩容的时候值改变size,不改变capacity,也就只调整_finish,不动_endofstorage。这是一种以空间换时间的思想。

resize的模拟实现:

【C++学习】vector的使用及模拟实现_第33张图片

在将三种情况实现出来后,resize便实现了,如上图所示。

empty:
【C++学习】vector的使用及模拟实现_第34张图片
如上图,当vector是空的时候,接口empty()的返回值是真,反之为假。

empty的模拟实现:

【C++学习】vector的使用及模拟实现_第35张图片
只要_finish和_start是相同的,就说明此时的vector是空的。

shrink_to_fit:

【C++学习】vector的使用及模拟实现_第36张图片
原本v1的size是1,capacity是10,在使用了shrink_to_fit以后,size和capacity相等了。

shrink_to_fit的作用就是将capacity变的和size一样的,也就是进行缩容,是一种以时间换空间的做法。

该接口是C++11才有的接口,本喵暂时就不进行模拟实现了。

vector的常用接口

【C++学习】vector的使用及模拟实现_第37张图片
STL中的vector除了上面提到的一些属性类的接口外,还有一些操作类的接口,也就是我们常说的增删查改,这也是一个数据结构中最核心的接口。

访问也是查的一种形式,而且访问是非常重要的,本喵来先给大家介绍一下vector的访问接口。

[]运算符重载:
[]在string详细讲解过,这里不多啰嗦,直接看演示:

【C++学习】vector的使用及模拟实现_第38张图片
可以看到,可以像访问数组一样去访问vector。

[]的模拟实现:

【C++学习】vector的使用及模拟实现_第39张图片
要严格检测是否发生越界。

【C++学习】vector的使用及模拟实现_第40张图片
可以看到,无论是写还是读都可以实现。

at:

【C++学习】vector的使用及模拟实现_第41张图片

可以看到,at的作用其实是和[]一样的,那么为什么又要有[]存在呢?

  1. []的可读性比at高

这一点毋庸置疑,我们肯定是喜欢阅读带有[]的代码,因为这样可以像访问数组一样来访问vector,而不是像at一样是函数调用,虽然本质上是一样的。

  1. 发生越界行为时,[]发生的断言错误,at是抛异常

[]是使用assert来防止越界的,而at在发生越界时会抛异常,并不会强制性的让程序停止。

头部和尾部数据的访问:

【C++学习】vector的使用及模拟实现_第42张图片
【C++学习】vector的使用及模拟实现_第43张图片
虽然使用[]也可以实现头部和尾部数据的访问,但是使用front和back的时候是不用知道尾部和头部下标的。

front和back的模拟实现:
【C++学习】vector的使用及模拟实现_第44张图片
front和back都是有重载函数的,一个是可读可写的,另一个是只读的,此时用const修饰了该接口函数,包括返回的也是被const修饰的引用类型,所以是不可以修改的。

find

在vector的STL中是没有提供find函数的,因为除string以外,其他容器查找的都是数据结构中的一个成员,并不是字符串之类的,所以这些数据结构共用一个find接口就可以,这个接口在官方提供的算法库中。

【C++学习】vector的使用及模拟实现_第45张图片

可以看到,在官方的文档中,连源码都给我们了,它就是通过迭代器来查找的指定元素的。

  • 查找到指定元素后,返回该元素的迭代器。
  • 没有找到指定元素时,返回该容器最后一个元素的下一个位置的迭代器,也就是end()。

【C++学习】vector的使用及模拟实现_第46张图片
使用find的查找情况如上图所示。

push_back就是一个非常典型的增,也是我们使用最多的,在前面本喵已经给大家详细介绍过了,并且也模拟实现了。除了这个以外,还有能够在任意位置插入的insert。

insert:
【C++学习】vector的使用及模拟实现_第47张图片
在官方提供的STL库中,insert有三个重载函数。

【C++学习】vector的使用及模拟实现_第48张图片
上图中,演示了使用insert在指定位置插入一个元素,插入多个元素,插入一段迭代器区间。

insert模拟实现:

虽然有多个接口,但是本喵只模拟实现一个:

【C++学习】vector的使用及模拟实现_第49张图片
可以看到,任意位置插入的接口中,存在着数据的挪动,如果头插一个元素的时候,需要将所有元素向后移动一个位置,代价是很大的。所以vector不建议进行频繁的头插,头插的时间复杂度是O(N2)。

【C++学习】vector的使用及模拟实现_第50张图片
可以看到,我们模拟实现的insert成功的实现了插入。

erase:

【C++学习】vector的使用及模拟实现_第51张图片
erase函数有俩个重载函数。

【C++学习】vector的使用及模拟实现_第52张图片

  • 使用算法库中的find找到3所在位置的迭代器,使用erase将该位置元素删除。
  • 将v2中除第一个和最后一个元素外都删除。

erase模拟实现:

这里仅实现一个删除指定位置的erase。

【C++学习】vector的使用及模拟实现_第53张图片
在删除以后,同样会发生数据的挪动。

【C++学习】vector的使用及模拟实现_第54张图片
成功删除了指定位置的内容。

clear:

【C++学习】vector的使用及模拟实现_第55张图片
clear的作用也是删除,但是它是将vector中的所有内容都删除,并且保留vector。

【C++学习】vector的使用及模拟实现_第56张图片
使用clear清空vector以后,size为0,但是capacity仍然保持不变。

clear的模拟实现:

【C++学习】vector的使用及模拟实现_第57张图片
实现起来非常简单。

pop_back:

图
仅有一个pop_back元素,没有重载函数。

【C++学习】vector的使用及模拟实现_第58张图片
该函数的作用就是将最后一个元素删除。

pop_back的模拟实现:

【C++学习】vector的使用及模拟实现_第59张图片
同样实现起来非常简单。

改就是从vector中找到某个元素,然后将其进行替换,一般都是先使用find找到某个元素,然后再使用[]或者at接口进行访问并修改。这些接口本喵在前面都讲解过,这里介绍一下没有讲解过的。

assign:

【C++学习】vector的使用及模拟实现_第60张图片
【C++学习】vector的使用及模拟实现_第61张图片
assign的作用就是将vector中原本的内容全部用新内容替换掉。该接口的使用频率并不高,本喵这里就不进行模拟实现了。

迭代器失效问题

  • insert内部由于扩容引起的迭代器失效(野指针):

【C++学习】vector的使用及模拟实现_第62张图片
使用我们模拟实现的insert在2的前面插入了一个100,此时是没有任何问题的,而且也是成功插入了。

【C++学习】vector的使用及模拟实现_第63张图片
此时重复上面的操作就崩溃了,这是什么原因呢?

首先我们来看俩次操作的不同之处:

  • 第一次插入数据时,vector中原本有3个数字,插入100后成了4个数字。
  • 第二次插入数据时,vector中原本有4个数字,插入100后成了5个数字。

俩次插入时,由于vector中原本的数据个数不同,所以发生扩容的情况也不同。第一次插入是不用扩容的,第二次插入需要扩容后才能插入。

【C++学习】vector的使用及模拟实现_第64张图片
第一次插入如上图所示,只需要将2和3向后移动一个位置,然后在空出来的pos处插入100即可。

【C++学习】vector的使用及模拟实现_第65张图片
第二次插入时,此时vector的容量已经满了,所以需要扩容后再插入数据。扩容时,开辟了一块新的空间,并且将原本空间释放了,但是pos指向的位置仍然是原本的位置。

  • 此时pos就成了野指针,也就是这里所讲的迭代器失效了。

为了避免这个问题,在扩容后需要更新一下pos的内容。

【C++学习】vector的使用及模拟实现_第66张图片
只需要让pos指向新位置的2即可,如上图所示。

【C++学习】vector的使用及模拟实现_第67张图片

在代码中进行如上图所示的操作即可。其中还增加了返回值,返回的就是pos迭代器,因为发生扩容以后pos的位置是会改变的,否则就找不到新的pos位置了。

  • erase之后引起的迭代器失效:

【C++学习】vector的使用及模拟实现_第68张图片
在VS2019上,当it迭代器处的位置被删除以后,it迭代器就不能再使用了,无论是读还是写,如上图所示,虽然没有直接报错,但是返回代码如红色框所示,说明它还是出错了。

【C++学习】vector的使用及模拟实现_第69张图片
同样的代码,在g++编译器下就不会报任何错误。这是好事还是坏事呢?

【C++学习】vector的使用及模拟实现_第70张图片
vector中的内容是1,2,3,4,删除其中的偶数。

【C++学习】vector的使用及模拟实现_第71张图片

成功删除了其中的偶数,但是这种操作在VS2019中是不被允许的。

  • 为了在任何平台下我们对代码都能够跑过去,在这里我们认为,使用erase删除指定位置后的迭代器是失效的,是不能再使用的。

那如果我们就要访问删除后迭代器的位置呢?

【C++学习】vector的使用及模拟实现_第72张图片

和insert一样,erase返回pos迭代器,此时返回的迭代器是不失效的,可以使用的。

【C++学习】vector的使用及模拟实现_第73张图片
如上图所示,erase返回的迭代器是可以使用的。

更深层次的深拷贝

【C++学习】vector的使用及模拟实现_第74张图片

  • vector的vector相当于是一个二维数组。

此时运行结果是正常的。

【C++学习】vector的使用及模拟实现_第75张图片
当插入第五个的时候,发生了错误,可以看到,打印出来的结果是乱的。

原因分析:

【C++学习】vector的使用及模拟实现_第76张图片

原本v2的结构如上图所示,v2中的四个元素分别指向一个vector。

【C++学习】vector的使用及模拟实现_第77张图片
在v2中插入第五个vector的时候发生了扩容。

【C++学习】vector的使用及模拟实现_第78张图片
而我们模拟的扩容是通过memcpy来复制原本vector中的内容,memcpy的机制是按照字节一个一个的复制。

  • v2在扩容的时候,创建了新的空间,是原来的2倍。
  • 将原本v2空间中的内容按字节复制到了新的v2空间。
  • v2中存放的内容本质是都是指针,所以按照字节复制以后,指针的内容并不会发生改变。
  • 此时v2的新空间中的指针和就空间中的指针指向的是相同的vector。
  • 并且memcpy以后会释放原本v2中指针指向的这些vector。此时v2新的空间中的指针指向的vector也就成了被释放的空间了。

这里在扩容的时候虽然发生了一次深拷贝,但是不够,需要进行更深一层的拷贝。

【C++学习】vector的使用及模拟实现_第79张图片
也就是让v2新空间中的指针指向新的vector,如上图中蓝色线所指。此时原来旧的vector便可以被释放了。

使用memcpy进行拷贝是行不通的。

【C++学习】vector的使用及模拟实现_第80张图片
如上图中红色框所示,这里使用重载的赋值运算符来复制原本v2中的内容。因为它会在赋值的过程中,给v2中的指针指向的vector开辟新的空间,就不怕旧的vector被释放了。

【C++学习】vector的使用及模拟实现_第81张图片
此时即使插入第五个vector也不会出错了。

总结

在有了string的基础以后,vector的使用还是非常容易的,所以在这篇文章中本喵采用了vector的使用和模拟并行的方式。对每一种数据结构的模拟实现,并不是为了造一个更好的轮子,而是为了能够对底层有更深的了解,从而能够更好的使用官方提供的这些数据结构的模板库。

你可能感兴趣的:(C++学习,c++,学习)