当前位置 : 主页 > 编程语言 > c语言 >

模拟实现vector

来源:互联网 收集:自由互联 发布时间:2023-08-25
首先要知道vector是什么 vector是什么 1.vector是表示可变大小数组的序列容器。 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问

首先要知道vector是什么

vector是什么

  1.vector是表示可变大小数组的序列容器。

  1. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
  2. 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大 小。
  3. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
  4. 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增 长。
  5. 与其它动态序列容器相比(deque, list and forward_list), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_list统一的迭代器和引用更好。


vector的实现

要实现一个容器第一步肯定还是确定vector的成员变量,并且因为vector和我们知道的普通数组不一样的就是,普通的数组内部只能储存整型的数据,但是vector你传入什么类型那么它就会实例化成能储存你传入类型的容器。由此实现vector需要使用到模板。

下面是vector的模拟实现基本代码:

template<class T>
	class vector
	{






	private:
		T* start;
		T* end;
		T* endofstorage;
	};

至于为什么vector的成员变量是三个指针。

下面我来解释这三个指针的用途:这三个指针指向的是同一片连续空间的不同位置。其中从start指向的是空间的开始,而end指向的是这个空间储存的有效数据的下一位。而endofstorage指向的是这篇空间的末尾。

无参默认构造和析构函数

下面是vector的无参的默认构造函数

vector()//首先实现一个无参的默认构造函数
			:start(nullptr)
			,end(nullptr)
			,endofstorage(nullptr)
		{}
~vector()
		{
			delete[] _start;//释放空间
			_start = _end = _endofstorage = nullptr;
		}//析构函数

然后下面我们来实现拷贝构造但是在实现拷贝构造之前肯定要让vector存在开辟空间的函数

reserve函数

//下面是和string一样的基本信息获取函数,连续的空间指针相减得到的就是两个指针之间的元素个数
		size_t size()
		{
			return _end - _start
		}

		size_t capacity()
		{
			return _endofstorage - _start;
		}
void reserve(size_t n)
		{
			//第一步需要判断是否需要扩容,而连续的空间指针相减得到的就是两个指针之间的元素个数
			size_t __capacity = capacity();//获取当前的容量
			size_t __size = size();//获取当前的有效数据长度
			if (n > __capacity)//需要扩容
			{
				T* tmp = new T[n];//首先创建一个n个大小的新空间
				if (_start)//如果原空间中存在信息,而非空
				{
					memcpy(tmp, _start, sizeof(T)*__size);//将原空间数据拷贝到新空间
				}
				_start = tmp;//将新空间赋值过去
				_end = _start + __size;
				_endofstorage = _start + n;//在这里就可以体现出我们提前将原空间有效数据记录下来的好处了
				//如果在这里你没有记录原空间的有效数据个数,而是采用函数计算,但是不要忘了现在的_start指向了一片新空间
				//而end指向的任然是老空间就会出现错误
				//所以这里需要记录老空间的有效数据个数
				delete[] _start;//删除原空间内存
			}

		}

在完成了reserve函数之后,我们就可以去完成push_back函数往vector中插入数据了。

push_back和[]函数

void push_back(const T& x)//尾插入一个数据
		{
			if (_end == _endofstorage)//判断空间是否足够
			{
				reserve(capacity() == 0 ? 4 : 2 * capacity());//这里的双目表达式,在capacity本省便为0的时候会创建4个大小的
				//空间,不然就按照空间两倍的大小扩容
			}
			*_end = x;//之后让*_end指向的那个空间填入x
			_end++;//再让_end++
		}

下面我们再来完成[]函数,之后来检测一下是否有错误存在。

T& operator[](size_t n)
		{
			assert(n < size());//首先判断是否超出有效数据范围。
			return *(_start + n);//然后返回这个数据即可
		}//并且我们现在重载的这个操作符既能读也能写

T operator[](size_t n) const
		{
			assert(n < size_t n);
			return *(_start + n);
		}//这个函数则是只有读的功能

下面是测试和运行截图(测试代码就不写了,可以在下图查看)

模拟实现vector_数据

正向迭代器的模拟

既然已经完成了基本的输入输出,下面我们就来完成迭代器的模拟实现。因为vector的空间也是连续的所以使用原生指针即可。

typedef T* iterator;
		typedef const T* const_iterator;//const迭代器是可以改变指针的指向,但是不能通过指针去修改值

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _end;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _end;
		}

最后是测试和运行截图:

模拟实现vector_数组_02

反向迭代器的模拟

和正向迭代器不一样,反向迭代器就不能使用原生指针了,因为反向迭代器的加加,反而需要我们去让指针减减,而反向迭代器的减减又需要我们去加加指针。

所以我们需要自己在写一个类用于反向迭代器的模拟(模板)。

如下:

template<class T, class ref,class ptr>
	struct Reserve_iterator
	{
		typedef Reserve_iterator<T, ref, ptr> self;
		T* _stu;//这个便是反向迭代器的成员变量是一个T* 的指针
		//然后下面我们来模拟一个指针能做的++,--,*,->,!=,==
		
		Reserve_iterator(T* tmp = nullptr)
			:_stu(tmp)
		{}
		
		self operator++()//前置++
		{
			_stu--;//将T*的指针减减,就是反向迭代器的加加
			return *this;
		}
		self operator--()//前置--
		{
			_stu++;
			return *this;
		}//和上一个一样

		self operator++(int)//后置++这里的int就只是表明这是一个后置++而已
		{
			self tmp(*this);
			_stu--;
			return tmp;
		}

		self operator--(int)
		{
			self tmp(*this);
			_stu++;
			return tmp;
		}

		//下面要重载*和->
		//这两个函数也是为什么要传入三个模板参数的原因
		//对于反向迭代器而言const版本和非const版本唯一的不同也就是,const版本的迭代器不能通过指针去修改值
		//那么对于const迭代器的*我们就可以返回const T&,非const迭代器就直接返回T&即可
    //所以我们设置了三个模板参数
		ptr operator*()
		{
			return (*_stu);
		}

		ref operator->()
		{
			return _stu;
		}

		bool operator!=(const self& b)
		{
			return _stu != b._stu;
		}

		bool operator==(const self& b)
		{
			return _stu == b._stu;
		}
	};

但是只完成这一个类也无法完成反向迭代器,最后还需要再vector的类中加入rbegin和rend函数

如下:

	typedef Reserve_iterator<T, T*, T&> reserve_iterator;
		typedef Reserve_iterator<T, const T*, const T&> const_reserve_iterator;
		
		reserve_iterator rbegin()//返回反向迭代器的开头
		{
			return (_end - 1);//因为_end指向的是有效数据的下一位
		}

		reserve_iterator rend()//反向迭代器的结束
		{
			return _start - 1;//这里的_start也需要减一,因为循环结束的条件是it和rend相等如果方向迭代器的rend返回的是开头元素的指针
			//那么当it和rend相等的时候不会进入循环,也就会导致原本开头的元素会被漏掉
		}
		
		const_reserve_iterator rbegin() const
		{
			return (_end - 1);
		}

		const_reserve_iterator rend() const
		{
			return _start - 1;
		}

那么为什么反向迭代器要三个模板参数呢?究其原因是为了区分const反向迭代器和非const反向迭代器

const 反向迭代器和非const的不同就在于两个函数,一个是*一个是->, 对于const迭代器而言。返回的 是const T& 而非const迭代器返回的是T& ,对于->函数const迭代器返回的是const T*。非const迭代器返回的是 T*。

所以我们将这几个不同的返回值作为模板参数,让我们只用写一个结构体就能实例化出,const反向迭代器和非const反向迭代器。

下面是测试和运行截图:

模拟实现vector_迭代器_03

复制拷贝函数和赋值=函数

下面我们来完成vector的复制拷贝函数以及=函数。

vector(vector<T>& b)
		{
			//我们需要拷贝b的内容,首先就需要拥有和b一样大的空间
			reserve(b.capacity());//创建空间
			for (int i = 0; i < b.size(); i++)
			{
				push_back(b[i]);//再将b里面的数据尾插入*this中完成拷贝。
			}
		}
		void swap(vector<T>& b)
		{
			std::swap(_start, b._start);
			std::swap(_end, b._end);
			std::swap(_endofstorage, b._endofstorage);
		}
		vector<T>& operator=(vector<T> b)
		{
			//这里的=我就使用了新式的写法,将要拷贝的vector在传参的时候,就将其拷贝完成,
			//然后让b和*this交换,即能把原空间释放(离开这个函数后b就会被销毁),也能完成*This的赋值
			swap(b);
			return *this;
		}

下面是测试截图:

模拟实现vector_数据_04

resize函数

和string的resize函数一样,resize也需要考虑三种情况。

	void resize(size_t n, T tmp = T())
		{
			if (n < size())//当n小于有效数据个数的时候需要我们删除元素
			{
				_end = _start + n;
			}
			else//需要我们增加数据,并且可能会伴有扩容
			{
				reserve(n);//如果n小于容量那么这个函数什么也不会做
				for (int i = size(); i < n; i++)
				{
					*(first + i) = tmp;//从最后一个元素开始往后赋值
				}
			}
		}

那么在这里我来解释一下,为什么tmp的全缺省使用的是T()而不是0,因为我们完成的vector是一个模板,它不是专门储存整型的容器,而是一个既能储存内置类型也能储存自定义类型的容器。所以这里的全缺省填的是匿名对象,让其能去调用这个T的构造函数,而在c++11之后内置类型也支持了默认构造。所以这里天的是一个匿名对象。

下面是测试实例:

模拟实现vector_数据_05

使用迭代器的构造函数

在库里面还有一种构造函数的方法,就是使用迭代器区间去初始化,并且这个迭代器是支持所有容器的迭代器的(使用模板)。

	template <class InputIterator>
		vector(InputIterator start,InputIterator,end)
		{
			while (start != end)
			{
				push_back(*start);//将迭代器指向的元素尾插到*this中
				start++;
			}
		}

下面是测试实例

模拟实现vector_迭代器_06

insert函数

和string的insert函数不同 它是插入到一个迭代器的前面。返回的也是插入数据的那个迭代器。

首先第一个是在某个迭代器前面插入一个数据:

iterator insert(iterator pos, const T& x = T())
		{
			//第一步还是要判断要插入的位置是否超出了有效数据的长度
			assert(pos >= _start);
			assert(pos <= _end);
		    //下面需要判断空间是否需要扩容
			if (_end == _endofstorage)
			{
				//记录pos是在哪一个位置的迭代器,因为扩容后
				//pos指向的那个空间就会被释放
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : 2 * capacity());
				pos = _start + len;//再次让pos指向它应该在的位置
			}
			//然后连续的空间需要插入还是使用从后向前的移动方法
			iterator tmp = _end;
			while (tmp >= pos)
			{
				*(tmp + 1) = *(tmp);
				tmp--;
			}
			*pos = x;
			_end++;
			return pos;
		}//这个是插入一个数据

下一个是在某个迭代器的前面插入n个数据

iterator insert(iterator pos, size_t n, const T& x = T())
		{
			assert(pos >= _start);
			assert(pos <= _end);
			//第一步创建一个临时的足够大的空间
			size_t len = pos - _start;//求出在pos前面存在多少数据
			T* tmp = new T[size() + n];//创建一个足够大的空间
			size_t tmpi = 0;
			for (; tmpi < len; tmpi++)
			{
				tmp[tmpi] = *(_start + tmpi);
			}//将pos前面的数据放到tmp中去
			while (n--)
			{
				tmp[tmpi++] = x;
			}//将n个数据放到tmp中去
			//最后将pos后的数据放到tmp中去
			for (int i = len; i < size(); i++)
			{
				tmp[tmpi++] = *(_start + i);
			}
			delete[] _start;//释放原空间储存
			_start = tmp;
			_end = _start + tmpi;
			_endofstorage = _start + n + size();
			return pos;
		}//在pos前面插入n个数据

因为使用迭代器完成插入一段数据如果是使用移动的方式,那么在插入数据时比较复杂,所以这里我才已经的是先创建一个足够大的空间,然后将pos前面的数据放到这个新数组中去,然后插入数据,最后将pos后面的数据再放到这个新创建的数组中去。由此完成。

还有一个方法就是移动数据

iterator insert(iterator pos, size_t n, const T& x = T())
		{
			size_t add = pos - _start;//首先找到pos在vector的哪一个位置
			size_t len = size();//记录原本的空间大小
			reserve(len + n+1);//扩容需要多扩容一个空间,否则在移动之后会导致
      //_size指向超出容量的空间导致析构出错,析构出错的原因也就是
      //_szie指向了超出容量的那一个空间,最后导致错误
			pos = _start + add;//重新找到pos
			for (int i = _end - _start; i >= add; i--)//移动数据
			{
				*(_start + i + n) = *(_start + i);
			}
			size_t times = n;
			while (times--)
			{
				*(_start + add) = x;
				add++;
			}
			_end = _start + len + n;
			return pos;
		}

需要注意的是如果使用这种方法俺么

下面是测试和运行截图:

模拟实现vector_数组_07

erase函数

首先erase函数有两个版本,一个删除迭代器此时指向的那一个元素,还有一个是删除一段迭代去区间。

首先来完成删除迭代器指向的那一个元素。

//删除pos位置的元素
		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _end);
			size_t len = pos - _start;
			for (int i = len; i < size(); i++)
			{
				*(_start + i) = *(_start + i + 1);
			}//从后往前移动数据
			_end--;
			return _start + len;//最后返回删除数据后的那个迭代器
		}//删除一个元素

删除一段迭代器区间的数据:

iterator erase(iterator first,iterator last)
		{
			assert(first >= _start);
			assert(first < _end);
			assert(last >= first);
			assert(last <= _end);
			if (last == _end)//删除从first开始往后的能删的所有元素
			{
				_end = first;//直接让_end等于_first即可
				return _;
			}
			else
			{
				//将last往后的元素从后往前移动即可
				for (int i = last - _start; i <size(); i++)
				{
					*first = *(_start + i);
					first++;
				}
				_end = first;
				return _start;
			}
		}

测试即运行截图:

模拟实现vector_数组_08

在完成了这两个函数之后,push_back和pop_back等函数就可以复用这两个函数完成了。我这里就不写了。


希望这篇博客对你能有所帮助,如果发现了任何错误,欢迎指出。我一定改正。

上一篇:日志等级类的测试
下一篇:没有了
网友评论