vector的模拟实现

目录

基础框架(包括正向迭代器类的实现)

begin()和end()

vector的反向迭代器类

rbegin()和rend()

增删查改(准确来说vector没有查,因为算法库里的find接口具备泛用性)

1.reserve、resize、push_back、size和capacity接口

2.operator【】

3.pop_back

4.insert  【关于迭代器失效的问题】

5.erase【迭代器失效问题】

6.关于迭代器失效的补充说明

成员函数

1.拷贝构造、利用迭代器区间构造、swap

2.赋值运算符重载

3.通过n个value构造vector的构造函数(内置类型也有构造函数)

关于拷贝构造和赋值运算符等函数还存在的致命BUG(vector>)

整体代码一览(可复制)


基础框架(包括正向迭代器类的实现)

如上图有三个成员_start,_finish,_end_of_storage,这三个都是迭代器类型的对象,即指针变量,因为在vecter中,元素在物理空间上是连续存储的,注意只要元素在物理空间上是连续的,则迭代器类就可以是原生指针类,所以vector的迭代器类就可以是原生指针,所以iterator _start 等价于 T* _start。 

注意如上图红框处的typedef定义的迭代器类必须写在public内部,因为我们是有在类外访问vector的迭代器的需求的,比如在类外定义一个迭代器对象vector<int>::iterator it,而typedef的类型是受到访问限定符的限制的,因此一定要在public里typedef迭代器类。

begin()和end()

注意这里万万不可将各个接口的返回值写成引用类型如const_iterator&和iterator&,这里是不能为了减少拷贝而写成引用的,因为调用这些接口的场景有可能对返回的迭代器进行自增或者自减操作,如下代码所示,如果返回引用类型,则这里++就会修改vector内部迭代器_start的指向,这是非常不合理的。

vector<int>v;

v.insert(++v.begin(),10);

vector的反向迭代器类

关于vector的反向迭代器类的讲解详情请见<<list的模拟实现(包括对迭代器知识的补充)>>一文。为什么在list中讲解呢?实际上在哪无所谓,因为反向迭代器是正向迭代器的适配器,所以反向迭代器类压根就不只是vector一个容器的事情。所以下文关于反向迭代器的话题,咱们只是随便过一过即可。

首先编写类模板reverse_iterator,代码如下。reverse_iterator是一个类模板,同时也是一个正向迭代器的适配器,能够通过【适配某容器的正向迭代器】这样的方法形成该容器的反向迭代器,vector的反向迭代器就是通过这样实现的。

#pragma once
#include<iostream>
using namespace std;
 
namespace mine
{
	template<class Iterator,class Arg>
	class reverse_iterator
	{
		typedef reverse_iterator<Iterator,Arg> r_iterator;//typedef受到访问限定符限制
	public:
		reverse_iterator(const Iterator& it)
			:_it(it)
		{}
 
		//前置++,反向迭代器的++it就对应正向迭代器的--it
		r_iterator operator++()
		{
			return r_iterator(--_it);
		}
 
		//前置--,反向迭代器的--it就对应正向迭代器的++it
		r_iterator operator--()
		{
			return r_iterator(++_it);
		}
 
		//后置++,反向迭代器的it++就对应正向迭代器的it--
		r_iterator operator++(int)
		{
			r_iterator temp(_it);
			_it--;
			return temp;
		}
 
		//后置--,反向迭代器的it--就对应正向迭代器的it++
		r_iterator operator--(int)
		{
			r_iterator temp(_it);
			++_it;
			return temp;
		}
 
		Arg& operator*()
		{
			Iterator temp = _it;
			temp--;
			return *temp;
		}
 
		Arg* operator->()
		{
			//写法1
			//return &operator*();
 
			//写法2
			Iterator temp = _it;
			temp--;
			return &(*temp);
		}
 
		bool operator==(const r_iterator& rit)
		{
			return _it == rit._it;
		}
 
		bool operator!=(const r_iterator& rit)
		{
			return _it != rit._it;
		}
	private:
		Iterator _it;
	};
 
}

有了reverse_iterator这个类模板后,我们就可以通过类模板和vector的正向迭代器适配出vector的反向迭代器类,代码如下。被typedef出的const_reverse_iterator和reverse_iterator就是vector的反向迭代器了。

#pragma once
#include<iostream>
#include<cassert>
#include"reverse_iterator.h"
using namespace std;


namespace mine
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		typedef reverse_iterator<const_iterator, const T> const_reverse_iterator;
		typedef reverse_iterator<iterator, T> reverse_iterator;

	private:
		iterator _start;//指向第一个元素位置的迭代器。
		iterator _finish;//指向最后一个元素后一个位置的迭代器。
		iterator _end_of_storage;//指向容器最大容量的后一个位置的迭代器。
	};

}

rbegin()和rend()

代码如下。

#pragma once
#include<iostream>
#include<cassert>
#include"reverse_iterator.h"
using namespace std;
 
 
namespace mine
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		typedef reverse_iterator<const_iterator, const T> const_reverse_iterator;
		typedef reverse_iterator<iterator, T> reverse_iterator;
 
        const_iterator begin()const//该接口也可以叫cbegin
		{
			return _start;
		}
 
		iterator begin()
		{
			return _start;
		}
 
		const_iterator end()const
		{
			return _finish;
		}
 
		iterator end()
		{
			return _finish;
		}
 
		reverse_iterator rbegin()
		{
			return reverse_iterator(end());
		}
 
		reverse_iterator rend()
		{
			return reverse_iterator(begin());
		}
 
		const_reverse_iterator rbegin()const
		{
			return const_reverse_iterator(end());
		}
 
		const_reverse_iterator rend()const
		{
			return const_reverse_iterator(begin());
		}
		
	private:
		iterator _start;//指向第一个元素位置的迭代器。
		iterator _finish;//指向最后一个元素后一个位置的迭代器。
		iterator _end_of_storage;//指向容器最大容量的后一个位置的迭代器。
	};
 
}

走到这里和vector的反向迭代器相关的代码就全部编写完毕了。

增删查改(准确来说vector没有查,因为算法库里的find接口具备泛用性)

接下来我们实现增加元素的接口push_back(),但谈到增加元素就少不了扩容,扩容的逻辑如下图所示。

1.reserve、resize、push_back、size和capacity接口

这里先实现两个简单的接口,如下图演示,这两接口要实现成const函数,不然const对象无法调用该接口。

如下图,注意vector的reserve,它的参数n可不代表字节的个数,而是代表元素的个数,比如vector用于存int,那么这里的n代表扩容n个int的空间,如果vector存vector,那么这里n代表扩容n个vector的空间。

上图中有一个判断,表示开辟新空间后,如果旧空间存在则要释放旧空间,避免内存泄漏。

对于扩容有一个需要注意的点,因为vector是一个类模板,可以存取不同类型的数据,所以在开空间的时候不能使用C语言的realloc或者malloc,因为T有可能是自定义类型,使用C语言的接口只能开空间,但无法调用构造函数完成对自定义类型的初始化。

memcpy第三个参数的单位是字节,表示对多少字节的数据进行值拷贝。

reserve实现完毕后,接下来就可以实现push_back了,如下图所示。

这里说一下,因为迭代器_finish的类型是T*,就是原生指针类型,此时也没有对operator*()和operator++()进行重载,因此(*_finish)解引用后一次拿到sizeof(T)字节的数据,等价于一个T类型的对象,对_finish进行++操作也是在_finish的基址上偏移sizeof(T)字节。

如果T是自定义类型,则在T类内部一定要实现赋值运算符重载operator=(),不然上图代码这里(*_finish)=x会报错。

接下来编写resize接口

和erase一样,当n值比当前vector中存在的有效数据的个数小时,不会缩容,只会修改_finish迭代器的指向。

resize还有更精简的写法,如下图所示。

2.operator【】

3.pop_back

如下图,只需要将_finish减一即可,不需要释放最后一个元素在堆上所占的空间,这里也无法释放,因为无法delete局部内存,所有元素在堆上占用的空间会统一在vector的析构函数中释放。

在_finish减一之前也没必要抹除_finish指向空间的数据,这里将数据修改成什么都没有意义,因为之后再push_back时会将旧数据覆盖。

4.insert  【关于迭代器失效的问题】

如上下图两种方式在挪动数据时,cur都永远指向目标位置,什么意思呢?

比如现在有1 2,我们想在1的位置上插入0,那么此时1 2就都要往后挪一个位置。2在1的后面,所以2先挪,现在2要往后挪一格,所以2的目标位置就是2的后一个位置,所以此时cur指向的位置就是2的后一个位置,这就叫做cur永远指向目标位置。然后在挪动数据的while循环中,让_start[cur]=_start[cur-1],然后让cur--。

上面两种方式实际上都还存在同一个问题,那就是迭代器失效问题,迭代器失效体现在两方面。

1.迭代器失效的第一方面

先看现象,如下图,右半部分只是注释了一行代码,进程就崩溃了,为什么呢?

这里直接上结论,因为在reserve时,最开始只开了4个空间,此时右半部分已经push_back了四个元素,那么再次insert时就会扩容。问题就出现在扩容这里,因为扩容后,_start等等迭代器已经发生了改变,即迭代器都指向新开辟的空间了,并且旧空间也被释放了,但是it迭代器并没有发生变化,依然指向旧空间,导致下图红框处的while判断肯定会出问题,可能不会进入循环,可能进入循环,如果进入循环,很可能出现越界导致崩溃。不仅如此,出了循环后(*it)=x也是对野指针解引用,导致程序崩溃。所以这就是体现迭代器失效的第一个方面。

因此,这里在reserve后,一定要将it迭代器指向新开辟的空间对应的位置,如下图就是insert接口最终的版本。

2.迭代器失效的第一个方面已经体现出,接下来说说迭代器失效体现的第二个方面。

如上文,insert接口的初始版本已经编写完毕,但还存在一个问题,如下图。

来解说一下上图的问题,首先push_back了4个元素,下一次向vector中添加元素时就会先扩容,再添加。insert插入4时,此时发生扩容并插入,扩容后,上图的it就已经指向旧空间了,所以再次向it指向的位置insert插入0时就会报断言错误,因为it不指向新空间。有人就会疑惑了?我们不是在insert接口里修改了it,让it指向新空间吗?没错,但你要清楚insert内部的it是上图it的一份拷贝,在insert内部修改it的指向并不会影响上图it的指向,所以这里再次使用上图的it迭代器去完成插入是会报错的。

那该怎么办呢?

现在我们可以将思维发散一下,我们可不可以将insert接口的参数iterator it改成iterator& it呢?这样如下图红框处,在insert内部修改it的指向,insert外的it不就一并修改了吗?

答案是万万不可,这里将insert接口的参数iterator it改成iterator& it后,虽然能解决当前场景下的问题,但在你意想不到的地方又会产生新的其他问题,如下图红框处就会报错,因为insert的参数iterator& it无法接收v.begin()返回的临时变量,想要解决该问题就必须把iterator& it改成const iterator& it,更改后,此时虽然能接收v.begin()返回的临时变量了,但又产生了新的问题,insert内部无法对it解引用修改it指向的元素了,这不就扯了嘛。所以我们模拟实现库中容器时,成员函数的声明一定要和库中接口的声明一致,不然会出各种意想不到的问题。

但说了老半天,还是没有揭示答案,对于迭代器失效体现的第二个方面(第二个方面是什么如果忘记了,回看上文),我们该如何解决呢?

解决问题前,我们需要知道几个知识点。

解决问题所需的知识点1:如下图1红框处,既然在迭代器it指向的位置插入一次数据后迭代器it可能会失效,也可能不会失效,那么我们就不要再次访问it,这样就绝对不会产生错误了。注意库里实现的vector的insert接口也存在迭代器失效的问题,如下图2所示,所以在用insert接口时,我们作为程序员是一定要知道迭代器失效这个知识点的,不然使用insert出错了都不知道原因。

这里随口提一下,库里实现的string同样有迭代器失效的问题,如下图中断言报错。

解决问题所需的知识点2:既然insert在迭代器it指向的位置插入一次元素后,vector是有可能扩容从而导致迭代器it失效的,导致我们不能再继续使用it迭代器,那么我们可以让insert接口返回一个新的迭代器,该迭代器指向刚插入的元素,我们直接使用新的迭代器不就行了,这样我们就可以不用关心vector内部是否扩容了。事实上STL即标准库就规定了vector的insert接口在插入成功后必须返回指向刚才插入元素的迭代器,如下图。

因此,我们模拟实现insert时,也要按照这个标准,所以最终版本的insert接口如下图所示。

终于到了解决问题的时刻了,有了最终版本的insert接口后,我们将如下左边的代码修改成右边代码即可解决上文中的问题。

这里再例举一个insert时迭代器失效的场景,如下图代码要求在所有偶数的前面插入偶数的二倍。

上图两个版本的程序都崩溃了。

上图左边崩溃的原因很好理解,循环到2判断为真,往2的位置插入4时要扩容,完成插入后因为已经扩容了,所以此时it迭代器指向已被delete释放了的旧空间,再去解引用导致越界崩溃。

那么上图右边不是提前开好了10个空间吗?按理说程序结束应该输出1 4 2 3 8 4啊?为什么也崩溃呢?

如下图,因为插入会先往后挪动数据,比如循环到_it指向2判断为真,要在_it指向的位置插入4时,把2往后挪了一次,插入完毕后_it就指向4了,但_it++后又指向了2,下一次循环也是同样的流程,最终导致循环的判断一直为真,一直插入4,直到10个空间被插满再次扩容后,迭代器就失效了,最后访问失效的迭代器指向的空间导致越界,程序就崩溃了。

崩溃原因已经找到,如何解决呢?

1.这里insert插入数据后有可能扩容,所以必须在insert后更新it迭代器,用it接收insert的返回值即可做到这点,因为insert返回一个指向插入刚元素的迭代器。

2.然后就是在插入后,it迭代器得++两次找到下一个新的数字进行判断。

整体代码如下。 

5.erase【迭代器失效问题】

先看看初始版的erase接口,如下两图所示。

我们这里实现的erase是不存在因为缩容导致迭代器失效的问题的,因为没有实现缩容的逻辑,但存在导致迭代器失效的其他方面的问题,如下图代码,找vector中的偶数并删除,因为vector中的数据不同导致结果也截然不同,第一种是正常运行结束并且结果正确,第二种是正常运行结束但结果不正确,第三种则是直接断言报错。

为什么会这样呢?

先说第一种情况,即正常运行结束并且结果正确。如下图是每一次循环的流程。

然后是第二种情况,即正常运行结束但结果不正确。如下图是每一次循环的流程。

然后是第三种情况,直接断言报错,如果没有断言语句则会因为越界访问导致进程崩溃。如下图是每一次循环的流程。可以发现下图最后一次循环时,因为_finish和_it刚好错过了,导致it!=v.end()判断再次为真,此时_it已经指向_finish的后面了,即越界了,此时还v.erase(it),所以进入erase函数后就因为it已经越界导致断言错误,如果不断言,则后序代码(*temp)越界访问导致进程崩溃。

那该如何解决上述这些问题让程序正常运行并且结果绝对正确呢?

答案:这里直接使用初始版的erase接口即可,然后需要修改一下主函数中for循环的逻辑就可以完成任务,如下图代码所示。

注意上图删除偶数这个程序,如果用库中的vector来跑是可能会因为erase缩容导致迭代器失效,进而越界访问导致程序崩溃的,因此,这里所说的【那该如何解决上述这些问题让程序正常运行并且结果绝对正确呢?】指的是用笔者实现的vector和erase接口来运行下图代码。

那该如何实现一个(我不管用谁开发的vector都能正常运行并且结果正确的)删除vector中偶数的程序呢?请继续往下看。

上面也说过,我们实现的erase不存在因为缩容导致的迭代器失效的问题,因为我们实现的erase内部没有缩容的逻辑,但不同版本的库中实现erase接口时,虽然可能性不大,因为以时间换空间在多数场景下不太可取,但还是有可能实现缩容的逻辑的,比如(_finish-_start)也就是vector中目前存在的元素的总数<=某个值后,为了节省空间,就去重新开辟一块小的空间,完成值拷贝后再删除旧的那块更大的空间。这时it迭代器因为还指向旧空间,所以再次访问it迭代器时,就会出现各种问题,也就是我们说的迭代器失效。

所以STL对于erase接口定了一条规定,我不管你内部逻辑如何实现,也不管是否缩容,但你在删除一个元素后,必须返回指向被删除元素的后一个元素的迭代器。

因此,我们最终版本的erase接口如下图所示。

 

有了最终版的erase,我们终于可以解决上文中的问题,可以实现一个万能通用的用于删除vector中偶数的程序了,如下图所示。

6.关于迭代器失效的补充说明

STL只是一个规范,规定了你必须存在某个功能,但是功能的底层是如何实现的它是不管的,因此不同的平台下,比如Linux的g++和Windows的vs对于同样的代码,如果代码在使用时不符合规范,则执行结果是完全有可能不相同的,但如果符合代码的使用规范,则执行结果一定是相同的。

因此上文中关于迭代器失效的场景,即有些在vs上崩溃的代码可能在Linux下的g++上编译是不会报错的。

【重点】这里还需要知道的是,在windows的vs环境下,在it迭代器指向的位置上insert插入或者erase删除一个元素后,这里vs是对it迭代器做了强制检查处理的,即使it迭代器指向的位置没有越界,也无法再次访问,如下图提前开了10个元素所需的空间,完全能满足两个元素的插入,但依然无法访问it迭代器。

成员函数

1.拷贝构造、利用迭代器区间构造、swap

编译器默认生成的拷贝构造是完成浅拷贝,也就是值拷贝,如下图是使用的编译器生产的拷贝构造,导致v和v1的迭代器成员都指向同一块空间。

这样会出现两方面的问题。

第一,v和v1在出作用域时都会调用析构函数,在析构函数内部会delete【】_start,而v和v1的_start指向同一块空间,所以_start被第一次delete变成野指针后,再次delete它就会越界访问导致进程崩溃。

第二,如果使用编译器默认生成的拷贝构造去构造v1,排除掉析构函数的影响,此时修改v1的迭代器成员所指向空间上的数据时,v的迭代器成员所指向空间上的数据同样也会被修改,因为它们的迭代器成员指向同一块空间。这不就扯了嘛。

综上所述,我们得自己实现一个深拷贝的拷贝构造,即需要额外开辟一块空间,然后把被拷贝对象中的数据拷贝到新空间上,这样两个对象就完全独立且值相等了。

注意这里额外开辟空间的大小不必和被拷贝对象完全相等,避免空间浪费,什么意思呢?比如有vector<int>v1内部new了1000个int的空间,但只存储了10个int的有效数据,我们在vector<int>v2(v1)拷贝构造v2时,如果v2深拷贝也开辟1000个int的空间,这不就是浪费嘛,因此v2深拷贝开辟空间时,可以选择只开辟能够容纳v1中有效数据的大小的空间,即开辟10个int的空间即可。但注意,虽然这样有效避免了空间的浪费,但也意味着v2之后插入数据相比于v1会有更多因为扩容产生的消耗,因此在拷贝构造时开辟空间的大小也是可以选择和被拷贝对象相等的,这样就避免了后序插入时产生的扩容消耗。所以综上所述,这里开辟空间大小的方案是可以自由选择的,它们各有优势,也各有短板。

如下图代码就是拷贝构造的传统写法,这里开辟空间的方案是选择更节省空间的方案。注意拷贝构造的参数必须是引用类型,不然会无限递归,直到栈溢出。

拷贝构造的传统写法还有一些比较另类的方式,如下图所示。

在范围for中使用引用是因为T类型可能是自定义类型,将v中T类型的对象直接拷贝给e可能会造成深拷贝,这样会产生较大的性能消耗,降低效率。在auto &e前加const是因为拷贝构造的形参v是const类型,e前不加const的话,e无法接收v传过来的元素。

接下来看看拷贝构造的现代写法。

先看看string拷贝构造的现代写法是怎么做的,如下图,当s2(s1)即使用s1拷贝构造s2时,它是先利用带参的其他构造函数,通过s1深拷贝构造出一个临时的string temp对象,然后将temp对象中的成员依次和s2交换,这样就完成了让s2深拷贝s1,并且让临时对象temp的成员的值都变成了交换前s1的成员的值,这里temp对象在出了拷贝构造的作用域后还会通过析构函数清理s1管理的空间,因为此时temp成员的值和s1成员的值进行了交换。

 

接下来我们想通过仿照string拷贝构造的现代写法来编写vector拷贝构造的现代写法。思考过后,发现下图的若干构造函数中只能通过利用迭代器区间构造vector的构造函数来实现现代写法。

那么先编写利用迭代器区间构造vector的构造函数,再通过它辅助实现拷贝构造。

可以发现如下图,利用迭代器区间构造vector的构造函数是一个函数模板,有人会说类模板里面还可以嵌套函数模板吗?还可以模板里嵌套模板吗?

答案是可以的,因为有这个需求。

那么有人会问vector内部不是通过typedef给vector定义了一个iterator迭代器类吗,干嘛把通过迭代器区间构造vector这个构造函数写成函数模板呢?

因为不止能够用vector的迭代器区间来构造vector,还可以用其他类比如string类的迭代器区间构造vector。换言之,如果这个构造函数不是一个函数模板,而是直接声明成vector(iterator begin,iterator end),那这个接口就只能利用vector的迭代器区间构造vector。

 

事实上上图代码并没有编写完整,直接使用上图的构造函数时,程序是会崩溃的,为什么呢?

答案:问题就出现在插入,第一次插入时会扩容,因为之前从未开辟过空间,而扩容时的逻辑如下图2。

可以发现如上图红框处,扩容开辟新空间后是会delete释放旧空间的。而正在被构造的对象的迭代器成员_start指向不为空,因为正在被构造的对象在初始化列表没有完成初始化,所以_start等迭代器成员都是随机值,又因为它们是随机值,所以在reserve的过程中清除旧空间时判断_start!=nullptr为真,所以delete【】_start清除旧空间就是越界访问,最后导致程序崩溃。

可能有人说,我程序没崩溃啊,那一定是因为你的编译器自作主张,帮你在构造函数的初始化列表把正在构造的vector对象的迭代器成员都初始化成了nullptr,所以进入构造函数的函数体内insert时才没有因为越界导致程序崩溃,但这并不代表你的代码没有问题。

因此最终版本的通过迭代器区间构造vector的构造函数如下图所示。

有了该构造函数,咱们就可以实现现代版本的拷贝构造了,如下图。

这里在初始化列表也要将被构造对象的成员全初始化为空,不然它的成员和temp对象的成员swap交换后,temp对象在出了拷贝构造的作用域后析构时也会因为delete释放野指针,即越界访问导致程序崩溃。

这里我们既为了方便,也为了符合STL的规范,我们给vector提供一个成员函数swap,如下图所示。

有了成员函数swap后,我们的现代版拷贝构造就可以复用swap了,如下图所示。

走到这里,现代写法的拷贝构造已经完全讲完了,但有人可能会很疑惑,现代写法的拷贝构造好麻烦啊,又要实现swap,还要实现通过迭代器区间构造vector的构造函数。注意,这里我们实现这俩接口不只是因为现代写法的拷贝构造需要复用他俩,而是因为vector类本来就要提供这些接口,拷贝构造的现代写法的只是顺便复用了他俩。

2.赋值运算符重载

赋值运算符重载和拷贝构造一样,对于需要深拷贝的类,直接使用编译器默认生成的赋值运算符重载函数会导致各种问题,比如重复析构,再比如修改一方导致另一方也跟着变,如忘记了请回顾拷贝构造。所以这里我们必须自己编写一个运算符重载函数。

先看传统写法,如下图所示。

再看看现代写法1,如下图所示。

假如v1=v2,先通过v2构造临时对象temp,然后让temp的成员和v1的成员依次交换,这样既让v1对象成员的值等于了v2对象成员的值,即完成了v1=v2,还通过temp对象出了operator=的函数作用域后会调用析构函数的特性释放了v1的旧空间。

如上图,这里只能将vector对象的成员依次swap,而不能直接将整个vector对象拿去swap,因为std的swap内部会调用operator=,如果我们在实现operator=时在内部std::swap交换整个vector对象,则会导致这两个接口无限调用对方,最后栈溢出,如下是流程图。

再看看更精简版的现代写法2

如下图所示,对比现代写法1,没有在函数体内定义temp对象,而是让形参v充当了temp对象的功能。可以发现现代写法1和现代写法2的本质是完全相同的,只不过代码更精简。

如果有人在调试时,发现在进入赋值运算符重载函数前会先进一次拷贝构造,请不要疑惑,这是正常的,因为现代写法2的赋值运算符重载函数的形参不是引用,调用函数时先传参,因此这里调试时先进入一次拷贝构造,然后再进入赋值运算符重载函数。

3.通过n个value构造vector的构造函数(内置类型也有构造函数)

注意,一定要将成员在初始化列表初始化成nullptr,否则成员就全是随机值,导致在reserve内部因为_finish和_end_of_storage不相等而开不出空间,然后在push_back内部对一个随机地址解引用*_finish=x就会越界导致程序崩溃。

这里push_back内部明明已经有了reserve的逻辑,为什么还要在push_back外部加一个reserve(n)呢?答案:为了避免push_back一次就扩容一次,这样频繁扩容消耗太大。

该构造函数的形参是一个半缺省的状态,value是有缺省值的,value的缺省值是对一个匿名对象的引用,而匿名对象是通过T类型的默认构造初始化的。

这里补充一点,你觉得int等内置类型有构造函数吗?

答案:在C语言里是没有的,但因为C++有了模板,如上图代码中const T&value=T()的T(),因为T要能够支持任意类型,所以也要支持 int(),所以在C++里,内置类型也是有构造函数的。既然内置类型也支持构造函数比如默认构造或者带参的构造,因此内置类型就可以如下图一般进行初始化。

在使用【通过n个value构造vector】这个构造函数时,有人可能会遇到一个小bug,接下来我们来讲讲。

下图3种情况,都是使用上图的构造函数构造vector。

可以发现上图中,实验1和实验2构造vector都成功了,并且打印出的值也符合预期。唯有实验3,它竟然都不是运行时报错,而是编译时报错即编不过,点击输出列表中的error后竟然还将错误追踪到了如下图的【通过迭代器区间构造vector】这个构造函数中,该构造函数和我们当前正在讲解的构造函数不是风牛马不相及吗?为什么报错是这样的呢?

答案如下文。

首先我们要知道,编译器在调用函数时,如果函数构成重载,则根据形参遵循最匹配原则来决定调用哪个函数。

然后当前我们调用构造函数的方式为 mine::vector<int>v(10,1); ,此时编译器会把字面常量10和1都当作int类型的值,而非unsigned int(也就是size_t)。所以编译器看到如上图的构造函数时,会觉得我实参的类型是int,和你函数的形参size_t类型不匹配,从而试图找到形参和实参更匹配构造函数。如果找不到,则编译器会把int隐式转换成size_t,再调用上图的构造函数构造vector,但不巧的是编译器找到了形参和实参更匹配构造函数,如下图的构造函数。

注意这里不要被InputIterator这个模板参数的名字给迷惑了,有人一看到InputIterator就以为它只能用于接收各种迭代器类型,这是非常错误的,InputIterator只是一个名字,和T这个名字没有任何区别,所以InputIterator不止可以表示迭代器的类型,它是可以表示任何类型的,因此InputIterator就可以表示int类型。当编译器看到如下图的构造函数的两个形参的类型都是int,然后一看10和1也都是int,所以mine::vector<int>v(10,1);这句代码最后就去调了下图这个构造函数,同时因为此时模板参数InputIterator是int,所以cur就是int类型的变量,所以程序编译不通过就是因为在下图红框处对int类型的变量解引用,这是个语法错误。

到了这里,bug出现的原因我们知道了,那我们该如何解决这个bug呢?

 

有人可能会提出一种方案,说将上图中n的类型从size_t变成int不就行了?这样搞确实能解决这边的问题,但是不符合STL的规范,如下图红框处,STL的标准就是将n定义成了size_t类型的变量,因此这种方法不合适。

那我们看看STL标准库中的源码,来分析该如何解决这个问题。

如上图红框,STL源码很鸡贼,直接提供了一个重载的【利用n个value构造vector】的构造函数,既然我不能将n从size_t类型修改成int类型,那我就再重载一个n为int类型的构造函数,如下图所示。

到了这里,这个bug也就讲解完毕了。但我再补充一点。

有人可能会疑惑,上文实验1中,如下图主函数中的vector对象在调用构造函数时为什么不去调用构造函数A,而去调了构造函数B呢?毕竟编译器把字面常量10看作int类型的值,而非size_t类型的值,当模板参数InputIterator被实例化成int时,10不就完美的和构造函数A匹配了吗?遵循更匹配原则,应该调用构造函数A啊,为什么调用构造函数B呢?

答案:的确,你的观点是十分正确的,但你还忘记了一点:构造函数A是有两个形参的,第二个形参还没有缺省值,但语句mine::vector<int>v(10);中只传了一个实参,因此最后只能调用具有缺省值的构造函数B完成初始化。

关于拷贝构造和赋值运算符等函数还存在的致命BUG(vector<vector<int>>)

从上文一路看到现在,事实上关于拷贝构造、赋值运算符重载函数、push_back、reserve函数,它们都还存在一个致命BUG。

如下图,当T类型为vector、string或者其他在拷贝时需要深拷贝的类时,vector<T>对象调用拷贝构造、赋值运算符重载、push_back、reserve、insert等内部逻辑需要拷贝T对象的接口时就会报错,如下图演示中调用push_back接口程序就报错了。

为什么呢?

因为vector<T>对象调用push_back、拷贝构造、insert、赋值运算符函数、reserve等等内部逻辑要拷贝T对象的接口时,都是通过调用memcpy接口完成的浅拷贝,如下图中各种接口内部都是调用memcpy。

接下来我们分析一下,当T为vector<int>或者string这样的需要深拷贝的自定义类型时,使用memcpy拷贝T对象的危害。

首先我们要知道memcpy完成的是逐字节的拷贝,然后当vector<vector<T>>对象调用【内部逻辑要拷贝T对象的】成员函数时,此时T的类型就为vector<int> ,这里拿拷贝构造举例,假如现在有vector<vector<T>>v1,然后vector<vector<T>>v2(v1)拷贝构造v2,这里要注意vector本质就是一个壳子,内部只有3个迭代器成员,迭代器指向的数据是不在vector中的,所以memcpy拷贝T对象,即如上图般v1拷贝v5,v2拷贝v6等都是直接拷贝_start等迭代器成员的值,这会导致多个vector<int>对象指向、管理同一块空间,比如上图连线中就是v1和v5管理同一块空间,v2和v6管理同一块空间,v3和v7管理同一块空间等等。但vv1对象的生命周期结束时,第一步会先调用析构函数释放vv1迭代器成员指向的v1、v2等vector<int>对象,然后第二步再释放vv自己3个迭代器成员自身所占的空间。在前面说的第一步中,也就是vv调析构函数delete[]_start释放v1、v2时,v1和v2等也得先调用析构函数delete[]_start释放v1和v2的_start分别指向的1,1 2 3这些数据,然后v1和v2自身所占的空间才能被vv的析构函数释放。所以关键就在于v1和v2等也得先调用析构函数delete[]_start释放v1和v2的_start分别指向的1,1 2 3这些数据,因为v5和v1的_start指向同一块空间,v6和v2的_start指向同一块空间,因此v5和v1析构时会delete同一块空间,v6和v2析构时也会delete同一块空间,进而造成释放野指针的问题、最后因为越界访问导致程序崩溃。

BUG的原因已经找到,该如何解决呢?

答案:先将vector的成员函数operator=()中拷贝T对象的方式从【memcpy】换成【调用T类对象的成员函数operator=()】,如下图的_start[i]=v._start[i]。

然后在vector<T>类的【内部逻辑有深拷贝T对象】的成员函数中,将拷贝T对象的方式从【memcpy】换成调用【vector类的成员函数operator=()】即可,如下图各种接口所示。

将memcpy修改成vector的成员函数operator=()后,此时vector<T>对象不管调用push_back、insert、拷贝构造还是调用其他【内部逻辑中需要拷贝T对象】的成员函数,都不会再出现错误,如下图所示。

将程序代码修改后,再次调用拷贝构造,假如现在有vector<vector<T>>v1,然后vector<vector<T>>v2(v1)拷贝构造v2,对比于之前错误的方式,即用memcpy完成对T对象的拷贝,现在正确的方式,即调用T类的成员函数operator=()完成对T对象的拷贝的逻辑地址空间如下图右半部分所示。

可是为什么在【内部逻辑需要拷贝T对象】的成员函数中将拷贝T对象的方式从memcpy换成调用vector的成员函数operator=()就可以解决BUG了呢?

答案:主要原因就是当vector<T>对象调用【内部逻辑需要拷贝T对象】的成员函数时,如果T是自定义类型,则vector的成员函数operator=()内部又会去调用T类的成员函数operator=()完成对T类对象的深拷贝。注意对于一个在拷贝时需要深拷贝的T类来说,它的成员函数operator=()在经过程序员的设计后是一定能支持在拷贝时是深拷贝的功能的。也正是因为T类的成员函数operator=()能够保证对T类对象在拷贝时是深拷贝,因此只要能保证vector<T>类的成员函数operator=()内部会调用T类的成员函数operator=(),就能保证vector<T>类对象在调用【内部逻辑需要拷贝T对象】的成员函数时一定不会出问题。

比如T为string,那么假如现在有如下图代码。

此时v1=v2时,就会先调用vector的operator=(),然后在vector的operator=()内部又会调用string类的成员函数operator=()完成对string对象的深拷贝。所以因为T可能是在拷贝时需要深拷贝的自定义类型,所以如下图,vector的operator=()中一定不能使用memcpy浅拷贝T对象,一定要写成_start[i]=v._start[i],即在vector的operator=()内部再次调用T类的成员函数operator=()完成对T对象的深拷贝。

可以发现上图只解决了拷贝构造和赋值重载函数传统写法的BUG,那么对于现代写法,我们该如何解决深拷贝的BUG呢?

可以发现现代写法中,swap只是交换了vector的迭代器成员,未涉及到拷贝的问题,因此只需解决【通过迭代器区间构造vector】这个构造函数内部的浅拷贝问题即可。

我们来观察一下【通过迭代器区间构造vector】这个构造函数内部又有哪些逻辑和接口调用。如下是拷贝构造现代写法的调用流程图。

通过上图流程我们可以得出该如何解决BUG的答案:我们只需将insert内部赋值时,如上图红框处的memcpy换成operator=(),然后因为insert内部调用了reserve,所以还需将reserve内部的memcpy换成operator=即可。

整体代码一览(可复制)

文件“reverse_iterator.h”如下。因为reverse_iterator是一个适配器,能够形成多种不同容器的反向迭代器,因此咱们单独放在一个文件里,供其他容器也能使用。

#pragma once
#include<iostream>
using namespace std;

namespace mine
{
	template<class Iterator,class Arg>
	class reverse_iterator
	{
		typedef reverse_iterator<Iterator,Arg> r_iterator;//typedef受到访问限定符限制
	public:
		reverse_iterator(const Iterator& it)
			:_it(it)
		{}

		//前置++,反向迭代器的++it就对应正向迭代器的--it
		r_iterator operator++()
		{
			return r_iterator(--_it);
		}

		//前置--,反向迭代器的--it就对应正向迭代器的++it
		r_iterator operator--()
		{
			return r_iterator(++_it);
		}

		//后置++,反向迭代器的it++就对应正向迭代器的it--
		r_iterator operator++(int)
		{
			r_iterator temp(_it);
			_it--;
			return temp;
		}

		//后置--,反向迭代器的it--就对应正向迭代器的it++
		r_iterator operator--(int)
		{
			r_iterator temp(_it);
			++_it;
			return temp;
		}

		Arg& operator*()
		{
			Iterator temp = _it;
			temp--;
			return *temp;
		}

		Arg* operator->()
		{
			//写法1
			//return &operator*();

			//写法2
			Iterator temp = _it;
			temp--;
			return &(*temp);
		}

		bool operator==(const r_iterator& rit)
		{
			return _it == rit._it;
		}

		bool operator!=(const r_iterator& rit)
		{
			return _it != rit._it;
		}
	private:
		Iterator _it;
	};

}

文件"vector.h"如下。阅读上文中vector<vector<int>>中存在的bug这部分内容后,可以知道,下面代码中所有注释掉的memcpy都是错误的写法,只是笔者没有将他们删除,而只是注释掉了。

#pragma once
#include<iostream>
#include<cassert>
#include"reverse_iterator.h"
using namespace std;


namespace mine
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		typedef reverse_iterator<const_iterator, const T> const_reverse_iterator;
		typedef reverse_iterator<iterator, T> reverse_iterator;
		vector()
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{}

		//传统写法
		//vector(const vector<T>& v)
		//{
		//	size_t size = v.size();
		//	_start = new T[size];

		//	//方式1调用库函数完成值拷贝
		//	//memcpy(_start, v._start, sizeof(T) * size);

		//	//方式2自己完成值拷贝
		//	for (size_t i = 0; i < size; i++)
		//	{
		//		_start[i] = v._start[i];
		//	}

		//	_finish = _start + size;
		//	_end_of_storage = _finish;
		//}

		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_end_of_storage, v._end_of_storage);
			std::swap(_finish, v._finish);
		}

		//现代写法
		vector(const vector<T>& v)
			:_start(nullptr)
			, _end_of_storage(nullptr)
			, _finish(nullptr)
		{
			vector<T>temp(v.begin(), v.end());
			swap(temp);
		}



		//传统写法
		//vector<T> operator=(const vector<T>&v)
		//{
		//	//因为是深拷贝,所以这里要重新开辟空间并删除旧空间
		//	size_t size = v.size();
		//	delete[]_start;
		//	_start = new T[size];

		//	//然后拷贝旧空间上的数据到新空间
		//	//拷贝数据的写法1
		//	//memcpy(_start, v._start, sizeof(T) * size);
		//	
		//	//拷贝数据的写法2
		//	for (size_t i = 0; i < size; i++)
		//	{
		//		_start[i] = v._start[i];
		//	}
		//	//完成旧空间到新空间的数据拷贝后,这里对迭代器的指向做处理
		//	_finish = _start + size;
		//	_end_of_storage = _finish;
		//	return *this;
		//}

		//现代写法1
		/*vector<T> operator=(const vector<T>& v)
		{
			vector<T>temp(v.begin(), v.end());
			std::swap(*this,temp);
			return *this;
		}*/

		//更简洁的现代写法2  v1=v2
		vector<T>operator=(vector<T>v)
		{
			swap(v);
			return *this;
		}

		vector(size_t n, const T& value = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				push_back(value);
			}
		}

		vector(int n, const T& value = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				push_back(value);
			}
		}

		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			InputIterator cur = first;
			while (cur != last)
			{
				insert(end(), *cur);
				cur++;
			}
		}

		~vector()
		{
			delete[]_start;
			_start = _finish = _end_of_storage = nullptr;
		}

		size_t size()const
		{
			return _finish - _start;
		}

		size_t capacity()const
		{
			return _end_of_storage - _start;
		}

		void reserve(size_t n)
		{
			if (n > _end_of_storage - _start) //等价 if(n >capacity())
			{
				T* temp = new T[n];
				if (_start != nullptr)
				{
					//做法1
					//memcpy(temp, _start, sizeof(T) * (_finish - _start)); //等价 memcpy(temp, _start, sizeof(T) * size());

					//做法2
					for (size_t i = 0; i < size(); i++)
					{
						temp[i] = _start[i];
					}
					delete[]_start;
				}
				_finish = temp + (_finish - _start);//因为_start=temp语句在下面,所以本行代码等价于 _finish = temp + size();
				_start = temp;
				_end_of_storage = temp + n;
			}
		}

		void resize(size_t n, const T& value = T())
		{
			if (n < size())//2 < 4
			{
				//写法1
				//_finish -= (size - n);// _finish-=(4-2)
				// 
				//写法2
				_finish = _start + n;;

			}
			else if (n > capacity())
			{
				reserve(n);
				while (_finish != _end_of_storage)
				{
					(*_finish) = value;
					_finish++;
				}
			}
			else// size()<=n<=capacity()
			{
				while (_finish < _start + n)
				{
					(*_finish) = value;
					_finish++;
				}
			}
		}


		void push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{
				reserve(_start == nullptr ? 4 : 2 * capacity());
			}
			//做法1
			(*_finish) = x;

			//做法2
			//memcpy(_finish, &x, sizeof(T));
			_finish++;
		}

		void pop_back()
		{
			assert(_finish > _start);
			--_finish;
		}

		const T& operator[](size_t pos)const
		{
			assert(pos < size());
			return *(_start + pos);  //等价于 return _start[pos];
		}

		T& operator[](size_t pos)
		{
			assert(pos < size());
			return *(_start + pos);  //等价于 return _start[pos];
		}

		const_iterator begin()const
		{
			return _start;
		}

		iterator begin()
		{
			return _start;
		}

		const_iterator end()const
		{
			return _finish;
		}

		iterator end()
		{
			return _finish;
		}

		reverse_iterator rbegin()
		{
			return reverse_iterator(end());
		}

		reverse_iterator rend()
		{
			return reverse_iterator(begin());
		}

		const_reverse_iterator rbegin()const
		{
			return const_reverse_iterator(end());
		}

		const_reverse_iterator rend()const
		{
			return const_reverse_iterator(begin());
		}
		iterator insert(iterator it, const T& x)
		{

			//方式一利用迭代器转换成下标实现
			//assert(it <= _finish && it >= _start);
			//判断是否需要扩容
			//if (_end_of_storage == _finish)
			//{
			//	size_t len=it- _start;			 
			//	reserve(_finish == 0 ? 4 : 2 * capacity());///1 2 3 
			//	it = _start + len;
			//}
			//挪动数据
			//size_t cur = _finish-_start;//cur下标对应最后一个元素的后一个位置
			//while (cur!=(it-_start))	//(it-_start)下标对应it迭代器指向的元素
			//{
			//	_start[cur] = _start[cur-1];
			//	cur--;
			//}
			//在it指向的位置赋值的方法1(深拷贝)
			//(*it) = x;

			//在it指向的位置赋值的方法2(浅拷贝)
			//memcpy(it, &x, sizeof(T));
			//_finish++;
			//return it;


			//方式二直接利用迭代器实现
			assert(it <= _finish && it >= _start);
			//判断是否需要扩容
			if (_end_of_storage == _finish)
			{
				size_t len = it - _start;
				reserve(_finish == 0 ? 4 : 2 * capacity());// 1 2 3
				it = _start + len;
			}
			//挪动数据
			iterator temp = _finish;
			while (temp > it)
			{
				*temp = *(temp - 1);
				temp--;
			}
			//在it指向的位置赋值的方法1(深拷贝)
			(*it) = x;

			//在it指向的位置赋值的方法2(浅拷贝)
			//memcpy(it, &x, sizeof(T));

			++_finish;
			return it;
		}

		iterator erase(iterator it)
		{

			//方式一利用迭代器转换成下标实现
			assert(it < _finish&& it >= _start);
			//挪动数据
			size_t cur = it - _start;//下标cur对应的位置就是it指向的位置
			while (cur != (_finish - _start - 1))// 下标(_finish - _start-1)对应最后一个元素的位置
			{
				_start[cur] = _start[cur + 1];
				cur++;
			}

			--_finish;
			return it;

			//方式二直接利用迭代器实现
			//assert(it < _finish&& it >= _start);// 1 2 3
			//iterator temp = it;
			//while (temp < _finish-1)
			//{
			//	(*temp) = *(temp + 1);
			//	temp++;
			//}
			//--_finish;
			//return it;
		}

	private:
		iterator _start;//指向第一个元素位置的迭代器。
		iterator _finish;//指向最后一个元素后一个位置的迭代器。
		iterator _end_of_storage;//指向容器最大容量的后一个位置的迭代器。
	};

}

文件“test.c”如下。

#include"vector.h";

void print(const mine::vector<mine::vector<int>>&v)
{
	for (const mine::vector<int>&e : v)
	{
		for (const int& i : e)
		{
			cout << i << ' ';
		}
	}
	cout << endl;
}
void main()
{
	mine::vector<int>v(10, 1);
	mine::vector<mine::vector<int>>vv;
	vv.push_back(v);
	print(vv);
	mine::vector<mine::vector<int>>vv1(vv);
	mine::vector<int>v1(10, 2);
	vv1.push_back(v1);
	print(vv);
	print(vv1);
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值