<C++> vector模拟实现

目录

前言

一、定义命名空间

二、构造函数

三、拷贝构造

四、赋值运算符重载

五、push_back && reserve

六、深拷贝问题

七、iterator 迭代器

1. 可读可写

2. 只读

八、operator[ ]

1. 可读可写

2. 只读

九、insert

问题:内部迭代器失效

十、erase

十一、resize 

总结


前言

        vector的使用与string大致相同,本节我们来参考stl中的vecor,模拟实现vector

        由于空间适配器较难,我们直接采用new、delete来完成开空间操作 ,后期再学习空间适配器。


一、定义命名空间

  • vector内的数据使用模板代替,因为vector内可能是指针、整形、string、vector等类型
  • 成员变量仿照stl内的vector成员变量,三个指针:start、finish、end_of_storage
  • 将指针类型重命名为iterator
  • 成员变量给出缺省值,因为是内置类型,所以可以不在初始化列表写出来
namespace my_vector
{
	template<class T>
	class vector
	{
	public: 

	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

二、构造函数

  • 成员变量初始化为nullptr,当然,因为给出了缺省值,这里可以不写初始化列表
		vector()
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{}
  • 初始化n个val,先初始化三个指针为nullptr,不然resize时会使用野指针,这也就体现了C++11可以给成员变量缺省值的优点,不用再为指针初始化为nullptr
		//先初始化为nullptr
		vector(size_t n, const T& val = T())
			//:_start(nullptr)
			//, _finish(nullptr)
			//, _end_of_storage(nullptr)
		{
			resize(n, val);
		}

 迭代器区间初始化构造函数

        因为迭代器指针类型不同,所以需要使用模板

		//迭代器模板,适用各种类型指针
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

        但是当我们构造一个v

vector<int> v(10, 1);

        编译器会优先调迭代器版本的构造函数,相比 vector(size_t n, const T& val = T()) ,这两个参数更匹配 vector(InputIterator first, InputIterator last),这是因为有两种选择,编译器选择了更加适合的,所以我们可以重载一个比它更适合的构造函数

		//重载一个更适合的版本
		vector(int n, const T& val = T())
		{
			resize(n, val);
		}

三、拷贝构造

  • 首先初始化列表初始化各指针为空
  • 再开空间,拷贝,最后修改指针 (此处的memcpy后面会再修改)
		//方法一:
		vector(const vector<T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{
			_start = new T[v.capacity()];
			memcpy(_start, v._start, sizeof(T)*v.size());
			_finish = _start + v.size();
			_end_of_storage = _start + v.capacity();
		}

        在深拷贝问题分析中,我们会再次修改拷贝构造函数  

现代写法:我们可以调用内部的函数,而不用手动开空间 

        这里将v中的每个元素取出,赋值给e,如果是自定义类型,那么将会调用它的拷贝构造函数赋值给e,是深拷贝,不是浅拷贝

		//方法二:
		vector(const vector<T>& v)
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			reserve(v.capacity());

            //这里将v中的每个元素取出,赋值给e
			//如果是自定义类型,那么将会调用它的拷贝构造函数赋值给e,是深拷贝,不是浅拷贝			       
            for (auto e: v)
			{
				push_back(e);
			}
		}

四、赋值运算符重载

  •  同模拟实现string的赋值运算符重载,我们将实参拷贝构造形参,交换this对象与形参对象,返回*this,使得this对象指向了拷贝构造的形参对象,而形参对象指向了原this,这样,在出了函数后,形参自动销毁,而*this的生命周期还在函数外,所以引用返回

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

		vector<T>& operator=(vector<T> v)
		{
			swap(v);
			return *this;
		}

五、push_back && reserve

  • 尾插前先判断空间是否还有剩余,如果满了,就reserve一段空间
  • 由于经常需要用到size、capacity的值,这些值需要指针相减计算,所以我们直接封装为size()、capacity() 成员函数,方便使用
  • reserve时,判断原数组_start是否为空。若不为空,再进行数据拷贝,并释放_start空间,最后更新成员变量
  • 在更新成员变量_finish时,如果直接使用size(),会出现错误,因为_start更新了,_finish还没更新,size()返回的是 旧的finish - 新的start会导致第一次更新的_finish为空,所以*finish = x 时就会出错。所以,我们可以提前记录size(),或者调换_start 和 _finish 的更新顺序
		size_t capacity() const
		{
			return _end_of_storage - _start;
		}

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

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				//如果原数组不为空,拷贝数据
				if (_start)
				{
                    //size(T)算的是成员变量的内存大小,数组内有多少元素没有影响
					memcpy(tmp, _start, sizeof(T) * sz);
					delete[] _start;
				}

				_start = tmp;
				//这里如果直接使用size(),会出现错误
				//因为start更新了,finish还没更新,size()返回的是 旧的finish - 新的start,
				//会导致第一次更新的finish为空,所以*finish = x 时就会出错
				//所以,提前记录size()即可,或者调换_start 和 _finish 的更新顺序
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}
		}

		//尽量用引用,因为vector里的数据可能是string、vector等较大的数据
		void push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}

			*_finish = x;
			++_finish;
		}

        size(T)算的是成员变量的内存大小,数组内有多少元素没有影响 

六、深拷贝问题

	vector<std::string> v;
	v.push_back("11111");
	v.push_back("22222");
	v.push_back("33333");
	v.push_back("44444");
	v.push_back("55555");

	for (auto& e : v)
	{
		cout << e << endl;
	}

        这里的 “11111” 会隐式类型转换为string,因为string有一个单参数的构造函数const char*,所以会发生构造临时变量的操作,具有常量性质,所以push_back参数是const T&,

        当vector内的数据类型为内置类型,进行扩容时reserve函数memcpy没有问题,是按字节拷贝。

        但是当vector内的数据是拷贝时需要深拷贝的自定义类型,扩容reserve函数的memcpy就有问题了,虽然vector数组是深拷贝,但是数组元素没有深拷贝,我们是按自定义类型字节大小进行的浅拷贝,拷贝的vector内的对象指向了被拷贝的vector对象的数组空间,又因为memcpy之后,立即调用了 delete[] _str,delete直接调用了数组内所有对象的析构函数,并释放数组空间,最终导致新拷贝的vector内的元素string的_str都指向了被释放的空间

解决方法:

        按元素个数遍历,逐个赋值运算符重载赋值给tmp

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				//如果原数组不为空,拷贝数据
				if (_start)
				{
					//size(T)算的是成员变量的内存大小,数组内有多少元素没有影响
					/*memcpy(tmp, _start, sizeof(T) * sz);*/
					for (size_t i = 0; i < sz; ++i)
					{
						//每个自定义类型数据都采用赋值运算符重载
						//若为内置类型,也没有影响
						tmp[i] = _start[i];
					}
					delete[] _start;
				}

				_start = tmp;
				/*这里如果直接使用size(),会出现错误
				因为start更新了,finish还没更新,size()返回的是 旧的finish - 新的start,
				会导致第一次更新的finish为空,所以*finish = x 时就会出错
				所以,提前记录size()即可,或者调换_start 和 _finish 的更新顺序*/
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}
		}

        所以,拷贝构造函数的memcpy也要跟着修改

		vector(const vector<T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{
			_start = new T[v.capacity()];

			/*memcpy(_start, v._start, sizeof(T)*v.size());*/

			for (size_t i = 0; i < v.size(); i++)
			{
				_start[i] = v._start[i];
			}

			_finish = _start + v.size();
			_end_of_storage = _start + v.capacity();
		}

         所以,我们可以再次理解vector<vector<int>> vv

七、iterator 迭代器

1. 可读可写

  • begin()、end() 分别对应 _start 和 _finish,返回两指针即可
		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

 2. 只读

  • 对于被const修饰的对象,在使用迭代器时,迭代器需要被const修饰,权限平移。也是为了与普通版本的iterator形成重载关系
		typedef const T* const_iterator;
     	const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

八、operator[ ]

1. 可读可写

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

2. 只读

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

九、insert

  • 第一步:对于insert函数,参考STL中vector的insert函数,参数为iterator迭代器,以及要插入的值,所以第一步要判断iterator是否在合法的范围
  • 第二步:进行扩容判断,如果要扩容就扩二倍,reserve()
  • 第三步:进行移位(这里略微体现了STL的vector为什么成员变量要用指针表示,而不用size_t 型的size以及capacity,因为对于size_t类型,如果pos指向_start,即pos == 0,那么end--时会出现end < 0 的情况,而size_t是无符号整形,-1是最大值,循环不会中断,陷入死循环)
  • 第四步:进行填充,并使_finish++,相等于size++
		void insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _finish);

			if (_finish == _end_of_storage)
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}

			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}

			*pos = x;
			++_finfish;
		}

问题:内部迭代器失效

        当我们插入的数据超过8个时(第一次扩容为4,第二次扩容二倍为8,第三次扩容为16),就会插入失败,是随机值。为什么呢?

        分析扩容处出现错误,因为每次扩容都是开辟一个新的空间,_start、_finish、_end_of_storage都会随之修改,而形参iterator pos是不变的,它还指向的原空间,成为野指针。

        解决:扩容前记录pos相对_start位置,并在扩容后更新

		void insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _finish);

			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;

				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);

				pos = _start + len;
			}

			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}

			*pos = x;
			++_finfish;
		}

        对于 push_back 我们可以复用 insert 函数

		void push_back(const T& x)
		{
			/*if (_finish == _end_of_storage)
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}

			*_finish = x;
			++_finish;*/
			insert(end(), x);
		}

新问题:外部的迭代器

        因为insert函数形参是iterator类型,是临时拷贝,如果扩容了,函数内部的pos会更新,但是在外部,实参pos还是原先的迭代器,如果此时在外部修改pos,相当于修改野指针,很容易出错,也就是说,在insert之后迭代器可能会失效(之所以是可能,是因为平台不一样扩容机制不一样,Linus扩容2倍,VS扩容1.5倍)

        所以,记住!insert以后就不要使用这个形参迭代器了,因为它很可能失效了,如果再进行 

	vector<int>::iterator pos = v.begin() + 3;
	v.insert(pos, 500);
	*pos += 10;

*pos += 10; 这是高危行为

        所以,STL的vector将insert的返回值设置为iterator,

        返回指向第一个新插入元素的迭代器,所以如果想继续修改,就用返回的迭代器,不要用实参传的迭代器

		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _finish);

			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;

				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);

				pos = _start + len;
			}

			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}

			*pos = x;
			++_finish;

			return pos;
		}

十、erase

  • 删除元素,移动后面元素即可
  • --_finish
		void erase(iterator pos)
		{
			assert(pos >= _start && pos < _finish);

			iterator begin = pos + 1;
			while (begin != _finish)
			{
				*(begin - 1) = *begin;
				++begin;
			}

			--_finish;
		}

erase的迭代器会失效吗?

        会,在特殊的情景会失效,当迭代器指向最后一个元素,删除元素后,迭代器就会失效。

我们先看一般情况:

void test2()
{
	vector<int> v;
	v.push_back(5);
	v.push_back(4);
	v.push_back(3);
	v.push_back(2);
	v.push_back(1);
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	vector<int>::iterator it = v.begin();
	v.erase(it);

	cout << *it << endl;
	++it;
	cout << *it << endl;

	for (auto& e : v)
	{	
		cout << e << " ";
	}
}

        可以看出,此时的迭代器没有失效,解引用和++操作都没有异常,但是当it指向最后一个元素时,问题就出现了

void test2()
{
	vector<int> v;
	v.push_back(5);
	v.push_back(4);
	v.push_back(3);
	v.push_back(2);
	v.push_back(1);
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	vector<int>::iterator it = v.begin() + 4;
	v.erase(it);

	cout << *it << endl;
	++it;
	cout << *it << endl;

	for (auto& e : v)
	{	
		cout << e << " ";
	}
}

​​​

        可以看到,当删除了最后一个元素后,第一次解引用是被删除的值 —— 1,相当于野指针(因为该空间按理来说已经 “释放” 了,对于数组删除元素,我们一般不抹除值,只进行覆盖),当 it++ 后再次解引用,it 还是野指针,但指向的空间是随机值,所以打印出了随机值

        对于VS的vector,它会强制判断,如果进行上面的操作,即使不是删除最后一个元素后再使用迭代器,VS都会直接报错;而g++不同,它不进行检查,但是不检查的行为,会导致出现很多问题。

        所以,同 insert 函数 erase 之后也不要使用迭代器,这也是高危行为

        返回一个迭代器,指向删除的最后一个元素之后的元素的新位置。如果操作删除了序列中的最后一个元素,则容器结束

		iterator erase(iterator pos)
		{
			assert(pos >= _start && pos < _finish);

			iterator begin = pos + 1;
			while (begin != _finish)
			{
				*(begin - 1) = *begin;
				++begin;
			}

			--_finish;
			return pos;
		}

        如果要在vector中删除偶数,那么迭代器要使用erase返回的迭代器,如果使用自己的迭代器,VS就不通过,代码没有移植性。

        其实对于erase,可能会出现其他平台,很珍惜内存空间,在删除了一半的数据后,会缩容,那么缩容时,迭代器肯定是失效的

        总结:vector在使用insert和erase之后,不能再访问这个迭代器,虽然insert之后迭代器有可能没有失效,但是我们都认为失效了,即访问结果是未定义的

十一、resize 

        对于vector来说resize是很重要的,在resize时,我们要指定默认初始为什么,这里就用到了模板和匿名对象。

void resize(size_t n, const T& val = T())

        对于自定义类型,构造时会调用其类型的默认构造函数,如果没有默认构造,那么这个resize函数就编译不通过,这也说明了,我们在写类的时候一定要写默认构造,不然坑害的是自己。

        对于内置类型,比如 int ,int( )在理论上是不行的,因为int并没有构造函数,但是这个需求是肯定的!所以C++的内置类型在C++出了模板之后就升级了,对于内置类型也有其构造函数,对于整形默认初始化为0,浮点型为0.0,指针类型为空。

int main()
{

	int i = 0;
	int j = int();
	int k = int(1);
	cout << "i = " << i << endl;
	cout << "j = " << j << endl;
	cout << "k = " << k << endl;

	return 0;
}

		//T类型匿名对象,对于自定义类型,调用它的默认构造,没有默认构造,那么程序就编不过
		void resize(size_t n, const T& val = T())
		{
			if (n < _finish)
			{
				_finish = _start + n;
			}
			else
			{
				reserve(n);
				while (_finish != start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}

 整体代码:

#pragma once
#include<iostream>
#include<stdlib.h>
#include<assert.h>
#include"ReverseIterator.h"
using namespace my_ReverseIterator;

namespace my_vector
{
	template<class T>
	class vector
	{
	public: 
		typedef T* iterator;
		typedef const T* const_iterator;
		//此时反向迭代器是自定义类型
		typedef ReverseIterator<iterator, T&, T*> reverse_iterator;
		typedef ReverseIterator<const_iterator, const T&, const T*> const_reverse_iterator;

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

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

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

		//先初始化为nullptr,因为成员变量给了初始值,所以可以不使用初始化列表
		vector(size_t n, const T& val = T())
			//:_start(nullptr)
			//, _finish(nullptr)
			//, _end_of_storage(nullptr)
		{
			resize(n, val);
		}

		//重载一个更适合的版本
		vector(int n, const T& val = T())
		{
			resize(n, val);
		}

		//迭代器模板,使用所有自定义类型的迭代器
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		vector()
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{}

		//方法一:
		vector(const vector<T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{
			_start = new T[v.capacity()];
			/*memcpy(_start, v._start, sizeof(T)*v.size());*/
			for (size_t i = 0; i < v.size(); i++)
			{
				_start[i] = v._start[i];
			}

			_finish = _start + v.size();
			_end_of_storage = _start + v.capacity();
		}
		
		方法二:
		//vector(const vector<T>& v)
		//	:_start(nullptr)
		//	, _finish(nullptr)
		//	, _end_of_storage(nullptr)
		//{
		//	reserve(v.capacity());
		//	for (auto e: v)
		//	{
		//		push_back(e);
		//	}
		//}

		//赋值运算符重载
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}

		vector<T>& operator=(vector<T> v)
		{
			swap(v);
			return *this;
		}

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

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

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

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				//如果原数组不为空,拷贝数据
				if (_start)
				{
					//size(T)算的是成员变量的内存大小,数组内有多少元素没有影响
					/*memcpy(tmp, _start, sizeof(T) * sz);*/
					for (size_t i = 0; i < sz; ++i)
					{
						//每个自定义类型数据都采用赋值运算符重载
						//若为内置类型,也没有影响
						tmp[i] = _start[i];
					}
					delete[] _start;
				}

				_start = tmp;
				/*这里如果直接使用size(),会出现错误
				因为start更新了,finish还没更新,size()返回的是 旧的finish - 新的start,
				会导致第一次更新的finish为空,所以*finish = x 时就会出错
				所以,提前记录size()即可,或者调换_start 和 _finish 的更新顺序*/
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}
		}
		//T类型匿名对象,对于自定义类型,调用它的默认构造,没有默认构造,那么程序就编不过
		void resize(size_t n, const T& val = T())
		{
			if (n < _finish)
			{
				_finish = _start + n;
			}
			else
			{
				reserve(n);
				while (_finish != _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}

		//尽量用引用,因为vector里的数据可能是string、vector等较大的数据
		void push_back(const T& x)
		{
			/*if (_finish == _end_of_storage)
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}

			*_finish = x;
			++_finish;*/
			insert(end(), x);
		}

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

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

		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _finish);

			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;

				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);

				//解决pos迭代器失效问题
				pos = _start + len;
			}

			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}

			*pos = x;
			++_finish;

			return pos;
		}

		iterator erase(iterator pos)
		{
			assert(pos >= _start && pos < _finish);

			iterator it = pos + 1;
			while (it != _finish)
			{
				*(it - 1) = *it;
				++it;
			}

			--_finish;
			return pos;
		}

	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

总结

        本文没有写反向迭代器,关于反向迭代器的构建和编写在这篇文章

<C++> 反向迭代器-CSDN博客

        多练习string、vector的模拟实现,下节我们学习list并模拟实现list,list实现过程中,类内部的封装更为复杂

        最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值