vector其实就是顺序表,它是在管理数组,并且它是一个类模板,可以实例化为不同类型的类,来供我们使用。STL标志模板库给我们提供了很多的成语函数接口来供我们使用,使我们编程的效率大大提高。
本喵在介绍它使用的同时,也会讲解它的底层原理,来模拟实现它,好让我们对vector有一个更深的了解。
官方库中提供上图所示的几种重载的构造函数,根据它们的函数声明就可以知道它们是如何使用的。
同样地,还提供了push_back函数来向vector中插入数据。
其中,形成类型const value_type&就是T&,T是模板参数,在实例化的时候可以是内置类型,也可以是自定义类型。
上图代码,向vector中插入数字,并且通过范围for打印出来。
- vector后的<>中的内容就是模板参数,可以是内置类型,如int,char,float等等
- 也可以是自定义类型,比如Date,甚至是vector等自定义类型。
vector的成员变量不和string的一样,并不是_size,_capacity_等,而是三个指针。
本喵来给大家看一下SJI版本的STL源码:
首先可以看到,vector是一个模板类,typedef的一些类型也是我们后面经常会用到的。
在模拟实现的时,将我们自己模拟的vector放在自己的命名空间wxf中,图中红色框中的内容是没有参数的默认构造函数。
为了向模拟实现的vector中插入数据,需要我们自己实现push_back()函数,如上图中所示。
- 红色框中是为了扩容而进行的三目运算。
- 当vector刚刚创建时,它里面是没有任何内容的,此时它的容量是0,此时需要给一个初始容量,这里本喵将其设置为4.
- 当vector中的容量不够,并且不是0时,进行二倍扩容,将容量变成原来的二倍。
还需要实现上图所示的俩个函数来辅助。
为了能够使用reserve函数来扩容,需要我们自己来模拟实现reserve函数,如上图所示。
- 红色框中,需要提前记录一下当前vector的size。
- 否则扩容后的_finish就会是0,因为temp加的值会是size(),而这个size是此时求出来使用的,由于_start已经发生了变化,所以求出来的值是一个和_start大小相等,符合相反的数。
可以自己去尝试一下,看看发生什么样的错误。
vector个string一样,也是动态变化的数据结构,所以就会存在扩容,下面本喵来给大家看看vector在不同平台下的扩容机制。
vs2019平台:
向vector中插入100个数据,当size和capacity相同的时候打印当前容量,因为此时会发生扩容,可以看到,每次扩大到之前容量的1.5倍左右。
本喵这里的扩容采用的是g++的机制,也就是严格按照之前容量的2倍来进行扩容。
除了没有参数的默认构造函数,还有使用迭代器区间来初始化类对象的构造函数。如上图所示,v2成功的用迭代器区间进行构造。
下面本喵来模拟实现一下它:
在类模板中,只要有需要是可以继续套模板的,如上图中的红色框中内容。
- 这里使用模板的原因是,为了通过迭代器区间来实现,至于迭代器的类型并没有固定为指针。
- 在vector和string中,迭代器的本质就是指针,但是在列表等其他数据结构中,迭代器的本质就不是指针了,所以这里使用的是泛型编程。
- 红色框中给是一个缺省值,该缺省值是T的匿名对象
- 如果是内置类型,比如int,它同样有默认构造函数,初始化后该int类型变量的值为0.
- 如果是自定义类型,在创建匿名对象的时候会调用它的默认构造函数。
但是使用char来实例化vector,并且使用该构造方式创建对象的时候,就不再报错,而且创建成功了,这是什么原因呢?
- 当使用int将vector实例化以后,构造函数的模板产生T就成了int类型。
- 在外部将int类型的5传给构造函数以后,由于第一个形参是size_t类型的,所以需要发生整型提升。
- 但是编译器此时认为,传给构造函数的俩个参数都是int类型,而此时构造函数的俩个形参一个是size_t,一个是int类型,要想匹配还需要整型提升第一个参数,比较麻烦。
- 编译器是比较懒的,不想多干活,所以它发现,下面的使用迭代器区间的构造函数,可以将模板参数推演为int类型,以此来供传过来的俩个int类型使用。
- 所以在函数内,对int类型解引用就发生了错误的间接寻址错误。
那么为什么,使用char类型来实例化vector就不会发生这个错误呢?
- 因为此时构造函数中的T被指定成了char类型,而另一个传过来的实参是int类型。
- 一个int类型,一个char类型,编译器为了少干活,就没有使用迭代器区间的构造函数,也就是没有推演模板参数,而是采用了将第一个参数整型提升为size_t的构造函数。
编译器也是懒狗,它会寻找工作量最少的方式来实现用户的要求,也就是会根据数据类型自行决定是推演模板参数类型,还是使用已有的函数。
解决这个问题的办法也是很简单,只需要重载一个int类型的构造函数即可,如上图红色框所示,此时模板参数实例化为int类型也不再报错。
拷贝构造函数:
构造函数学习了以后,按照成员函数类型,还需要有拷贝构造函数。
STL库中的拷贝构造函数声明如上,它的形参是一个vector的引用。
如上图所示,使用v1来构造v2,可以看到v2中的内容和v1一模一样。
模拟实现拷贝构造函数:
上图所示的是拷贝构造函数的现代写法,也就是抓壮丁,在string的模拟实现时,本喵详细讲解过。
这里使用的swap函数不是标准库中的,所以需要我们自己实现:
- 拷贝构造函数中的交换函数,之所以不使用标准库中的sawp,是为了减少系统的开销。
- 如果模板参数T是一个自定义类型的时候,使用库中的swap代价就会非常大,因为自定义类型在这个过程中会发生拷贝。
- 在模拟实现的swap中再使用库中的swap时,仅仅是指针变量直接的交换,发生拷贝也就4个字节大小,代价并不大。
必须先实现俩个const迭代器的成语函数,如上图所示,因为拷贝构造的形参x是const类型的vector,此时它的this指针是被const修饰的,它的成语函数begin和end得到的迭代器也必须是被const修饰的,否则就会发生权限的放大,是不被允许的。
赋值运算符重载函数:
如上图所示,成功的将v1赋值给了v2,其实这样看来,赋值运算也是属于构造的一种。
赋值运算符重载的模拟实现:
这里的形参不能使用引用,否则会将赋值的对象改变。这里会发生拷贝构造,创建出一个新的对象v,但是这个v不在栈区上,而是在堆区上,将this指针指向的内容和v进行交换。
上图中的接口全部都是和容量有关的,如size,capacity,reserve接口在前面介绍构造函数的时候已经介绍了,下面本喵来介绍一下其他没有介绍的。
resize:
vector的resize和string的resize是一样的。
- size < n < capacity:仅调整size,也就是只改变_finish的值。
- n > capacity:size和capacity都发生了改变,也就是发生了扩容
- n < size:仅调整size,只改变_finish。
原则: 缩容的时候值改变size,不改变capacity,也就只调整_finish,不动_endofstorage。这是一种以空间换时间的思想。
resize的模拟实现:
在将三种情况实现出来后,resize便实现了,如上图所示。
empty:
如上图,当vector是空的时候,接口empty()的返回值是真,反之为假。
empty的模拟实现:
只要_finish和_start是相同的,就说明此时的vector是空的。
shrink_to_fit:
原本v1的size是1,capacity是10,在使用了shrink_to_fit以后,size和capacity相等了。
shrink_to_fit的作用就是将capacity变的和size一样的,也就是进行缩容,是一种以时间换空间的做法。
该接口是C++11才有的接口,本喵暂时就不进行模拟实现了。
STL中的vector除了上面提到的一些属性类的接口外,还有一些操作类的接口,也就是我们常说的增删查改,这也是一个数据结构中最核心的接口。
访问也是查的一种形式,而且访问是非常重要的,本喵来先给大家介绍一下vector的访问接口。
[]运算符重载:
[]在string详细讲解过,这里不多啰嗦,直接看演示:
[]的模拟实现:
at:
可以看到,at的作用其实是和[]一样的,那么为什么又要有[]存在呢?
这一点毋庸置疑,我们肯定是喜欢阅读带有[]的代码,因为这样可以像访问数组一样来访问vector,而不是像at一样是函数调用,虽然本质上是一样的。
[]是使用assert来防止越界的,而at在发生越界时会抛异常,并不会强制性的让程序停止。
头部和尾部数据的访问:
虽然使用[]也可以实现头部和尾部数据的访问,但是使用front和back的时候是不用知道尾部和头部下标的。
front和back的模拟实现:
front和back都是有重载函数的,一个是可读可写的,另一个是只读的,此时用const修饰了该接口函数,包括返回的也是被const修饰的引用类型,所以是不可以修改的。
在vector的STL中是没有提供find函数的,因为除string以外,其他容器查找的都是数据结构中的一个成员,并不是字符串之类的,所以这些数据结构共用一个find接口就可以,这个接口在官方提供的算法库中。
可以看到,在官方的文档中,连源码都给我们了,它就是通过迭代器来查找的指定元素的。
- 查找到指定元素后,返回该元素的迭代器。
- 没有找到指定元素时,返回该容器最后一个元素的下一个位置的迭代器,也就是end()。
push_back就是一个非常典型的增,也是我们使用最多的,在前面本喵已经给大家详细介绍过了,并且也模拟实现了。除了这个以外,还有能够在任意位置插入的insert。
insert:
在官方提供的STL库中,insert有三个重载函数。
上图中,演示了使用insert在指定位置插入一个元素,插入多个元素,插入一段迭代器区间。
insert模拟实现:
虽然有多个接口,但是本喵只模拟实现一个:
可以看到,任意位置插入的接口中,存在着数据的挪动,如果头插一个元素的时候,需要将所有元素向后移动一个位置,代价是很大的。所以vector不建议进行频繁的头插,头插的时间复杂度是O(N2)。
erase:
erase模拟实现:
这里仅实现一个删除指定位置的erase。
clear:
clear的作用也是删除,但是它是将vector中的所有内容都删除,并且保留vector。
使用clear清空vector以后,size为0,但是capacity仍然保持不变。
clear的模拟实现:
pop_back:
pop_back的模拟实现:
改就是从vector中找到某个元素,然后将其进行替换,一般都是先使用find找到某个元素,然后再使用[]或者at接口进行访问并修改。这些接口本喵在前面都讲解过,这里介绍一下没有讲解过的。
assign:
assign的作用就是将vector中原本的内容全部用新内容替换掉。该接口的使用频率并不高,本喵这里就不进行模拟实现了。
使用我们模拟实现的insert在2的前面插入了一个100,此时是没有任何问题的,而且也是成功插入了。
首先我们来看俩次操作的不同之处:
- 第一次插入数据时,vector中原本有3个数字,插入100后成了4个数字。
- 第二次插入数据时,vector中原本有4个数字,插入100后成了5个数字。
俩次插入时,由于vector中原本的数据个数不同,所以发生扩容的情况也不同。第一次插入是不用扩容的,第二次插入需要扩容后才能插入。
第一次插入如上图所示,只需要将2和3向后移动一个位置,然后在空出来的pos处插入100即可。
第二次插入时,此时vector的容量已经满了,所以需要扩容后再插入数据。扩容时,开辟了一块新的空间,并且将原本空间释放了,但是pos指向的位置仍然是原本的位置。
- 此时pos就成了野指针,也就是这里所讲的迭代器失效了。
为了避免这个问题,在扩容后需要更新一下pos的内容。
在代码中进行如上图所示的操作即可。其中还增加了返回值,返回的就是pos迭代器,因为发生扩容以后pos的位置是会改变的,否则就找不到新的pos位置了。
在VS2019上,当it迭代器处的位置被删除以后,it迭代器就不能再使用了,无论是读还是写,如上图所示,虽然没有直接报错,但是返回代码如红色框所示,说明它还是出错了。
同样的代码,在g++编译器下就不会报任何错误。这是好事还是坏事呢?
成功删除了其中的偶数,但是这种操作在VS2019中是不被允许的。
- 为了在任何平台下我们对代码都能够跑过去,在这里我们认为,使用erase删除指定位置后的迭代器是失效的,是不能再使用的。
那如果我们就要访问删除后迭代器的位置呢?
和insert一样,erase返回pos迭代器,此时返回的迭代器是不失效的,可以使用的。
- vector的vector相当于是一个二维数组。
此时运行结果是正常的。
当插入第五个的时候,发生了错误,可以看到,打印出来的结果是乱的。
原因分析:
原本v2的结构如上图所示,v2中的四个元素分别指向一个vector。
而我们模拟的扩容是通过memcpy来复制原本vector中的内容,memcpy的机制是按照字节一个一个的复制。
- v2在扩容的时候,创建了新的空间,是原来的2倍。
- 将原本v2空间中的内容按字节复制到了新的v2空间。
- v2中存放的内容本质是都是指针,所以按照字节复制以后,指针的内容并不会发生改变。
- 此时v2的新空间中的指针和就空间中的指针指向的是相同的vector。
- 并且memcpy以后会释放原本v2中指针指向的这些vector。此时v2新的空间中的指针指向的vector也就成了被释放的空间了。
这里在扩容的时候虽然发生了一次深拷贝,但是不够,需要进行更深一层的拷贝。
也就是让v2新空间中的指针指向新的vector,如上图中蓝色线所指。此时原来旧的vector便可以被释放了。
使用memcpy进行拷贝是行不通的。
如上图中红色框所示,这里使用重载的赋值运算符来复制原本v2中的内容。因为它会在赋值的过程中,给v2中的指针指向的vector开辟新的空间,就不怕旧的vector被释放了。
在有了string的基础以后,vector的使用还是非常容易的,所以在这篇文章中本喵采用了vector的使用和模拟并行的方式。对每一种数据结构的模拟实现,并不是为了造一个更好的轮子,而是为了能够对底层有更深的了解,从而能够更好的使用官方提供的这些数据结构的模板库。