vector模拟

vector的函数接口实现和string差不多,当我实现了string接口函数再来看vector就会亲切许多。

vector和string不同的地方是vector是用到了类模板的知识 ,如下代码,首先将我们的类封装在命名空间中,然后就是声明类模板,类模板的作用范围就是紧挨着下一行的类或者函数,所以整个vector用的都是一个未定的类型T,由于类型未定,我们在写成员函数的时候都要考虑这函数是不是适用于所有的数据类型,不然我们模拟的就和库不一样了,这是不利于我们理解vector的底层的。要熟悉一个类就先看看它的成员变量。

cpp 复制代码
namespace my_vector
{
	template<class T>
	class vector
	{
	public:
        typedef     T*         iterator;
		typedef const T* const_iterator;  迭代器重命名
	private:
		iterator _start  =  nullptr;//指向空间起始地址
		iterator _finish =  nullptr;//指向最后有效数据的下一个位置
		iterator _end    =  nullptr;//指向最大容量的下一位置
	};
}

一 构造和析构函数

1.默认构造函数

根据c++11新规定,成员变量可在声明时给缺省值,这使得我们在写下面的默认构造函数和拷贝构造函数时可以不初始化成员变量,当然这是因为我们知道我们的成员变量都是内置类型,如果有自定义类型,又需要显示调用构造函数,那就不能偷懒了,得在初始化列表初始化这个自定义类型了。

cpp 复制代码
public:
		
		vector()
		{
			;
		}

2.最熟悉的拷贝构造函数

cpp 复制代码
       vector(const vector<T>& value)
	{
		reserve(value.capacity());

       一次性开辟好空间

		for (int i = 0; i < value.size(); i++)
		{
			push_back(value[i]);
		}
	}

3.用一段迭代器区间初始化vector对象

该构造函数用了模板,在调用的时候就会实例化出一份函数供使用,但不是说函数仅在调用时才实例化,当我们vector<int>指定类型的时候,编译器就已经把类内函数参数带T的替换成了int, 这使得部分函数完成了实例化,不再是模板函数, 但下面这个函数得靠传参才能推导实际类型,毕竟迭代器的类型和vector存储内容的类型是不一样的,可能传vector<int>的迭代器,也可能传string的迭代器,只要这些迭代器指向的数据大致符合int即可用来给vector<int>初始化。

cpp 复制代码
   template<class InuputIterator>
传两个迭代器,简单点来看认为是两个指针
   vector(InuputIterator first,InuputIterator last)
  {
		知识储备不多,实现的毕竟比较稚嫩,一时也不知道如何处理
     使得vector<list>可以用该函数初始化,下面使得必须是空间连续的迭代器才可用来初始化

	 reserve(last - first); 两指针之差为元素个数,一次性开辟好空间,
                           免得多次扩容浪费时间。
	 while (first!=last)
	{
		push_back(*first);复用push_back即可
		first++;
	}
  }

默认构造和拷贝构造都各有特色,不会出现调用冲突**,但是第三和第四个构造函数在有些情况下则会冲突,比如vector<int> v1(10,1),我们本意是用十个1初始化v1,但是两个参数都是int类型(数字默认是int),这个时候下面的构造函数4一个参数是size_t,一个是const int(const T已被实例化为const int),构造函数的优先级是,有实例化的且参数匹配排第一,没有实例化的就用模板看看生成的参数和传的是否匹配,都没有才看看传的参数可不可以类型转换一下,勉强挤一挤的,构造函数4的第一个参数要符合传的int,只能让int隐式类型转换,所以构造函数3优先级高,当编译器就会去调用它,但是该构造函数由于我们把传入参数当做指针,对int解引用也就出问题了。**

解决方法:

**(1)如下写个第一个参数类型为int的构造函数,虽然第二个参数带T,但在写vector<int>就会用int代替T,所以下面这个函数就变成适合vector<int> v1(10,1)调用的实例化函数,**已经实例化且参数匹配的函数优先级比构造模板函数3高,就不会出现对int解引用了。

(2)把vector<int> v1(10,1)改为vector<int> v1(10u,1),不再写一个函数,而是使用者自己强转,就不用编译器做隐式类型转换了,使得下面的这个构造函数的优先级高于构造函数3。

4.用n个T类型的变量初始化vector<T>

cpp 复制代码
​

	vector(int n ,const T& value=T())这里直接复用resize即可。
   {
		resize(n,value);
   }

	vector(size_t n, const T& value=T())
	{
		resize(n, value);
	}

​

5.析构函数:

cpp 复制代码
		~vector()
		{
			delete[]_start;
			_start = _finish = _end = nullptr;
		}

delete我之前只是记忆了一下如果指针指向的是动态开辟的空间,会去调用对应的析构函数,还没太理解是什么意思,直到我尝试在自己写的vector里面存vector<int>,在调试的过程中我发现了一个非常奇怪的事。

cpp 复制代码
void TestVector8()//测试delete调用析构函数
{
	my_vector::vector<my_vector::vector<int>> vv1;
	my_vector::vector<int> v1;
	my_vector::vector<int> v2;

	v1.push_back(1);
	v2.push_back(2);

	vv1.push_back(v1);
	vv1.push_back(v2);
}

我调试到上面函数结束的时候发现我进了析构函数,我在vs是先释放局部变量v1和v2,然后释放vv1,vv1在遇到delete[]_start没有执行下一句,而是跳转到了析构函数开头从新执行,我当时都懵了,第一次看到语句执行往回走的,后来我根据this指针推出是delete[]_start去清理_start指向的vector<int>对象了,所以又去调用析构函数,所以我调试的时候显示的是又回去了(由于我们初始空间开了四个,但是有两个都未添加数据,成员变量存的值均为空,但都会走一遍析构函数)是调用了新函数我觉得可以从调用堆栈来论述。 如下图:析构函数调用析构函数,在调试状态下去窗口选择调用堆栈即可查看(快捷键为ctrl+alt+c)。

迭代器的配套函数

cpp 复制代码
		iterator begin()
		{
			return _start;
		}
          end()函数返回的是最后一个有效字符的下一个地址,所以是_finish,而不是_end
          佐证是我们用库的迭代器都是!=end()为结束条件,且都把有效字符打印完。

		iterator end()
		{
			return _finish;
		}

		const_iterator begin()const  给const对象调用的,返回的指针
                                      不具有修改vector内数据的功能
		{
			return _start;
		}
              
		const_iterator end()const
		{
			return _finish;
		}

三 []重载(返回指针指向内容的引用)

cpp 复制代码
        size_t size()const
		{
			return _finish - _start;
		}

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

		T& operator[](size_t pos)
		{
			assert(pos<size()&&pos>=0);
			return _start[pos];
		}

      const对象返回const引用,因为const修饰对象只是其成员的值,如_start的值不可改变,
                            但其指向的内容我们仍可以修改,所以要对引用进行const修饰

		const T& operator[](size_t pos)const
		{
			assert(pos < size() && pos >= 0);
			return _start[pos];
		}
		

四 insert

在说insert前,我们要说一个概念叫迭代器失效,在我这迭代器的实现中迭代器就是一个指针,它指向一块空间,我们都知道插入数据是会扩容的,扩容很大概率会异地扩容,那就是换地方,搬家了,但是insert函数有个参数iterator pos它是记住了内容搬家前的地址的,这时候都搬家了,地址已经无法找到内容了,也就是迭代器失效了,这是迭代器失效的一个场景。

cpp 复制代码
	iterator insert(iterator pos,const T& v)
		{
			assert(pos >= _start && pos <= _end);判断插入位置的合法性

			if (_finish == _end)//扩容
			{
				int len = pos-_start;  

提前计算pos相对_start的位置,不然扩容后_start变了,无法判断pos相对_start要加什么下标
 
				int capacity = _end - _start;
				reserve(capacity == 0 ? 4 : capacity * 2);
				//更新pos
				pos = _start + len; 
      扩容后,_start变了,插入位置的地址也要更新,提前计算的len就是为了这里做准备
      这步是为了防止内部的迭代器失效
			}

			//挪动数据
			iterator end = _finish;
			while (end >= pos)
			{
				*(end) = *(end-1);
				end--;
			}
			(*pos) = v;
			_finish++;

			return pos; 为了防止外部传的迭代器失效,我们返回pos去更新外部的迭代器
		}

扩容函数

cpp 复制代码
	void reserve(size_t n)
		{
			size_t capacity = _end - _start;

旧的容量我们是用指针求的,有些时候如果我们用默认构造了一个对象,直接用reserve扩容
对内置类型未处理,在这里capacity可能就用野指针计算了。所以构造函数为了安全都要初始化成员

			if (n > capacity) 新空间大于就空间才扩容
			{
				int len = _finish - _start;
				iterator tmp = new T[n];
				//memcpy(tmp, _start, len*sizeof(T));浅拷贝
				if (_start)//_start不为空才要拷贝_start指向空间的数据
				{
					for (size_t i = 0; i < size(); i++)
					{
						tmp[i] = _start[i];
					}
				}
				delete[]_start;
				_start = tmp;
				_finish = _start + len;
				_end = _start + n;
			}
		}

五 赋值函数

模板还有个补充概念,先前说成员函数的实例化会不仅仅在调用的时候实例化 ,当我们定义一个类型为vector<vector<int>> vv1,类就会把类内的T换成vector<int>, 那当我们在外部定义了vector<int> v1, 此时编译器会用模板根据T为int类型再实例化出一个类,这个时候内存其实是有两份不同类型的类,重点来了,然后用push往vv1内放v1, 遇到扩容也会经历上面通过赋值将_start的数据复制到tmp中去,此时调用的是自定义类型vector<int>的赋值,调试的时候你会发现此时的赋值是去vector模板类中的模板赋值函数,如果你在外面用vv1赋值给同类型的vv2, 调试也是进来这个模板赋值函数,也就是说虽然这个模板赋值函数会生成多个实例化函数,但我们看不见,调试的时候虽然都是进到这里,但底层调用的是各自的赋值函数。(我之前调试看到两个不同类型的赋值到同一个函数,我还以为是凑合凑合共用呢)

cpp 复制代码
	   void swap(vector<T>& value)
		{
			std::swap(_start, value._start);
			std::swap(_finish, value._finish);
			std::swap(_end, value._end);
		}

          v1=v2 ,此处的value是v2用拷贝构造函数构造出来的
		vector<T>& operator=(vector<T> value)//此处不能为引用
                                           
                           我们是将局部变量value的空间交换给v1,并让v1的原空间交给value析构
                           非常巧妙!!!
                                           
		{
			swap(value);
			return *(this);为了支持连续赋值,返回结果
		}

六 erase

尾插尾删复用即可

cpp 复制代码
	    void pop_back()
		{
			erase(--end());
		}

        void pop_front()
		{
			erase(begin());
		}

		iterator erase(iterator pos)
		{
			assert(pos >= _start && pos <_finish); 检查合法性

			iterator begin = pos+1;

         挪动数据覆盖
			while (begin<_finish)
			{
				*(begin-1) = *(begin);
				begin++;
			}
			_finish--;
			return pos;
		}

除了插入数据导致异地扩容会出现迭代器失效,当我们删除一个元素,即便该空间没有被释放,
vs强制检查,认为用了erase函数后的这个迭代器,他是失效的,必须接受返回值更新。
若是删除最后一个元素,此时返回的pos访问的值一般是原来的数据,毕竟vector的删除数据不是抹除该位置**,为了文明使用,访问数据就应该在_finish在_start指针之间,当迭代器到末尾了就不应该再使用。**

七 resize

cpp 复制代码
   //T()是valued的缺省引用,匿名对象调用构造                                          
void resize(size_t n,const T& value=T())
{
	 if (n > size())
	{
		if (n > capacity())
	   {
	  	  reserve(n);
	   }
		//将size扩大到n
	  iterator it = _finish;
	  _finish = _start + n;  

	  while (it!=_finish)
	 {                    添加数据
		*it = value;
		it++;
	  }
    }		
		_finish = _start + n;
}

终于写完了,本文中有些点我当时调试困惑了许久,差点都放弃了,幸好解决了,希望能对大家有用。

相关推荐
labuladuo5206 分钟前
AtCoder Beginner Contest 372 F题(dp)
c++·算法·动态规划
DieSnowK9 分钟前
[C++][第三方库][httplib]详细讲解
服务器·开发语言·c++·http·第三方库·新手向·httplib
StrokeAce2 小时前
linux桌面软件(wps)内嵌到主窗口后的关闭问题
linux·c++·qt·wps·窗口内嵌
家有狸花5 小时前
VSCODE驯服日记(三):配置C++环境
c++·ide·vscode
dengqingrui1236 小时前
【树形DP】AT_dp_p Independent Set 题解
c++·学习·算法·深度优先·图论·dp
C++忠实粉丝6 小时前
前缀和(8)_矩阵区域和
数据结构·c++·线性代数·算法·矩阵
ZZZ_O^O6 小时前
二分查找算法——寻找旋转排序数组中的最小值&点名
数据结构·c++·学习·算法·二叉树
小飞猪Jay9 小时前
C++面试速通宝典——13
jvm·c++·面试
rjszcb9 小时前
一文说完c++全部基础知识,IO流(二)
c++