C++STL之vector的模拟实现

由于vector和string的接口使用方法类似,这里便不再详细讲解vector各种接口的使用了,可以参考之前所发布的string的使用,或者从官方文档中获取详细的使用方法.

目录

vector介绍

构造函数(有参,无参,迭代器)

析构函数

获取容量函数capacity()

获取有效数据大小size()

reserve()

resize()

push_back()

[]运算符重载

迭代器相关

begin()、end()

pop_back()

 insert()&&迭代器失效问题

erase()&&迭代器失效问题


vector介绍

这里直接开始讲解vector的模拟实现.

在此之前,先对vector进行一个简单的介绍.

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

这些后面模拟接口实现的时候,就会对这些概念有一个大概的了解.

先来说一下vector模拟实现的整体框架.

vector内部是指针实现的,所以类成员有三个:指向数据开始的指针,指向数据结束的指针以及指向数据容量大小的指针.它的迭代器也是一个原生指针.

主要是实现其构造、拷贝构造、赋值运算符重载、析构函数,容量相关类的接口函数,[]重载(访问下标),对数据的修改操作(插入,删除等等).

下面是所有的接口声明.

这里我们把迭代器提前命名好了.

namespace hmylq
{

	template<class T>

	class vector

	{

	public:

		// Vector的迭代器是一个原生指针

		    typedef T* iterator;

			iterator begin();

			iterator end();


			/construct and destroy///

			vector();

			vector(int n, const T& value = T());

			template<class InputIterator>
    
	    	vector(InputIterator first, InputIterator last);
    
			vector(const vector<T>& v);

			vector<T>& operator= (vector<T> v);

			~vector();

			/// capacity

			size_t size() const ;

			size_t capacity() const;

			void reserve(size_t n);

			void resize(size_t n, const T& value = T());



			///access///

			T& operator[](size_t pos);

			const T& operator[](size_t pos)const;



			///modify/

			void push_back(const T& x);

			void pop_back();

			void swap(vector<T>& v);

			iterator insert(iterator pos, const T& x);

			iterator erase(Iterator pos);

	private:

		iterator _start; // 指向数据块的开始

		iterator _finish; // 指向有效数据的尾

		iterator _endOfStorage; // 指向存储容量的尾

	};

}

构造函数(有参,无参,迭代器)

先来看无参的构造函数:初始化列表将类成员3个指针全部初始化为空即可.

		vector()
			:_start(nullptr)
			,_finsh(nullptr)
			,_end_of_storage(nullptr)
		{
			;
		}

有参构造函数:两个参数,分别是数据的总大小以及数据的内容.

比如传一个5,1.就相当于在vector里面存储1,1,1,1,1

这里给了两个缺省值.

		vector(int n, const T& value = T())//T()是一个匿名对象,如string,会调用它的构造函数,内置类型如int,也有构造函数,默认为0.
			:_start(nullptr)
			, _finsh(nullptr)
			, _end_of_storage(nullptr) 
		{
			reserve(n);
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

这里的reserve()和push_back()函数我们后面会模拟实现.

还有最后一个是迭代器区间构造.

首先创建一个模板参数,我们认作它为迭代器类型,然后利用从起始位置不断++直到到了数据的结尾结束,每次从中插入每个数值.

		//迭代器区间构造
		vector(Inputlterator first, Inputlterator last)
			:start(nullptr)
			,_finsh(nullptr)
			,_end_of_storage(nullptr)
		{
			for (first != last)
			{
				push_back(*first);
				++first;
			}
		}

这里是保证到end时就一定结束,保证所有数据可以被访问到. 

析构函数

析构函数比较简单,首先delete掉数据的内容,再将三个指针指向的内容全部置为空即可.

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

获取容量函数capacity()

由于类成员是指针,我们没办法直接返回它,可以利用指针的相减来获得空间的大小.

用指向容量大小的指针 - 指向数据开始的指针既容量大小.

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

获取有效数据大小size()

这个和capacity()函数类似,只不过是用指向数据结束的指针 - 指向开始的指针.

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

reserve()

这个函数的作用是扩容,当用户输入的n小于原本容量时则不需要任何操作,仅当n大于_end_of_storage时才进行扩容

首先新开辟一块空间,然后将原来的数据拷贝到新空间中,再分别更新_start,_finsh,_end_of_storage,但是更新的时候会有一些问题.

先来看一个常见的错误版本.下面是错误的代码!只是拿来示例一下问题.

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				T* tmp = new T[n];//新开辟一块空间
				if (_start)
				{
					memcpy(tmp, _start, sizeof(T) * size());//将原来的数据拷贝到新空间
					delete[] _start;//释放掉旧数据,此时三个指针都指向空.
				}

				_start = tmp;//将开始的指针指向数据的开始
				_finsh = tmp + size();//***wrong***
				_end_of_storage = _start + n;
			}
		}

以上代码看起来没有任何问题,但是在wrong的那一行,会有一个bug.

我们前面已经把空间释放掉了,所以3个指针都指向空,但是在执行_finsh = tmp + size();这条语句时,size()是如何计算的呢,size()是_finsh - _start,而此时只有_start更新了,而_finsh依然是空,所以替换过来,相当于抵消了_start,结果还是_finsh,是空.

所以解决办法是一开始先保存上数据的大小.正确代码如下:

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				if (_start)
				{
					memcpy(tmp, _start, sizeof(T) * sz);
					delete[] _start;
				}
 
				_start = tmp;
				_finsh = _start + sz ;
				_end_of_storage = _start + n;
			}
		}

resize()

这个和string类型的很像,

1.如果n > 总容量,则直接扩容到n,并且初始化

2.如果n > size(),但是小于容量,直接初始化即可

3.如果n < size(),需要删除数据


		void resize(size_t n, const T& value = T())
		{
			if (n > capacity())
			{
				reserve(n);
			}
			if (n > size() )
			{
				while (_finsh < _start + n)
				{
					*_finsh = value;
					++_finsh;
				}
			}
			else
			{
				_finsh = _start + n;
			}
		}

push_back()

这个函数的作用是尾部插入一个数据,这个比string的简单,因为vector只能插入一个数据.

首先判断是否需要扩容,再直接将*_finsh的值赋值为val,然后++_finsh,这里注意,_finsh指向的是最后一个数据的下一个位置,所以可以直接赋值.

		void push_back(const T& x)
		{
			if (_finsh == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity()  * 2);
			}
			*_finsh = x;
			++_finsh;
		}

[]运算符重载

我们需要像数组一样下标访问或修改某一个元素,那么需要对[]进行重载.方法很简单,就是返回第pos个位置的值.

这里有两种,一种是普通的返回,还有一种是const修饰的,以便能适用const所修饰的调用.

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

const修饰的

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

迭代器相关

上面也说过,迭代器内部也是原生指针,所以

typedef T* iterator;

begin()、end()

迭代器中的begin和end就是返回指向数据开始的指针,和数据结尾的指针,如下

		iterator begin()
		{
			return _start;
		}

end()

		iterator end()
		{
			return _finsh;
		}
		

既然有了迭代器,那么也就对应的可以进行范围for遍历了.

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

范围for其实是一个傻瓜式的替换.

无论类里的迭代器的开始写的什么,最终都会被替换成begin(),所以如果自己实现的迭代器的开始函数名字不是begin(),则会报错,因为范围for被替换成了begin(),而没有begin()这个成员

pop_back()

思路很简单,直接--_finsh就相当于把最后一个元素删除掉了.

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

 insert()&&迭代器失效问题

这里insert是插入一个元素,然后有两个参数,一个是迭代器类型的pos位置,另一个是要插入的值val.

我们的思路是:

1.如果插入后空间足够,我们从pos位置依次向后挪动一个空间,再把空出来的这一个空间插入val.

2.如果插入后空间不足,则需要扩容,扩容之后再进行第一步的操作,但这样会存在迭代器失效问题.

先来看这段存在迭代器失效问题的错误代码

		void insert(iterator pos, const T& x) // 注意迭代器失效
		{
			assert(pos >= _start);
			assert(pos <= _finsh);

            //扩容
			if (_finsh == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);//如果需要扩容,pos位置则会改变,需要更新
			}
            //插入操作
			iterator end = _finsh - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			_finsh++;
		}

然后我们测试一段代码:

void test_vector5()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
    //v.push_back(5);

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

	//查找3
	auto p = find(v.begin(), v.end(), 3);
	//如果查找到了,则在3的位置插入一个30
	if (p != v.end())
	{
        //在p位置插入数据以后就不要再去p了,因为可能已经失效了.
		v.insert(p, 30);
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

运行起来,发现程序崩溃了.

这是为什么呢?

我们看insert()代码中,首先插入了4个数据,此时要将新数据val插入其中,此时需要扩容

扩完容之后,_start,_finsh,_end_of_storage都已重新更新了位置,并且将原空间中的数据拷贝到新空间中,释放掉旧空间,但注意此时pos依然还在指向旧空间的位置,这就使pos成为了野指针.

画图来理解一下

所以既然pos没有变化,那我们解决方案就是:在扩完容之后重新更新一下pos即可,具体做法为:先保存pos的长度,扩完容之后,再将_start+pos即可.

void insert(iterator pos, const T& x) // 注意迭代器失效
		{
			assert(pos >= _start);
			assert(pos <= _finsh);

			if (_finsh == _end_of_storage)
			{
				size_t len = pos - _ start;//先保存一下长度
				reserve(capacity() == 0 ? 4 : capacity() * 2);//如果需要扩容,pos位置则会改变,需要更新
				pos = _start + len;
			}

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

在那段测试代码中,我们插入了一个在p位置插入了一个30,但如果此时再插入一个40,便会再次出现问题,因为p的位置插入30时已经失效了,但有人会说我们不是更新了pos吗在扩容之后,我们仔细的看一下,pos只是一份形参!形参的改变不会影响实参,所以p并没有改变.

那可能就有人又说了,直接给pos加上引用不就可以了吗,这样pos更新,p也会跟着更新了。

这么做确实正确的解决了这个问题,但是如果下面这段代码该怎么处理呢?

insert(v.begin(),3);

这句代码是说想在begin()的位置插入一个3,但是由于begin()是传值返回,会形成一份临时拷贝数据,这个数据具有常性,而我们的就是普通的iterator&,权限不可以放大.这样又会造成了问题.

那我们在iterator前面加一个const不就可以了吗,就具有了常性,但加上之后扩容的时候我们该怎么修改这个p呢?这又是个问题.

所以正如以上所说:在p位置插入一个数据后,就不要再去使用这个p了,可能会失效.

erase()&&迭代器失效问题

erase()是删除某个位置的数据,思路也很简单,就是从pos+1位置开始,每次覆盖掉前一个位置.

	    //STL 规定erase返回删除位置的下一个位置的迭代器	
        iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finsh);

			iterator begin = pos + 1;
			while (begin < _finsh)
			{
				*(begin - 1) = = *begin; 
				++begin;
			}
			--_finsh;
            return pos; 
		}

虽然这个代码本身不涉及迭代器失效问题,但看下面一段代码:需要删除所有的偶数.

看代码

比如原来的数据是1,2,3,4.

 我们可以看到问题是出现在每次erase之后,它的后一个数据已经移动过来了,但是it依然++,相当于it跳过了一个数据直接访问下一个,所以正确的方法是:

 这样就成功解决了.

这里总结一句:insert/erase pos位置,不要直接访问pos,一定要更新!

直接访问,可能会有各种出乎意料的结果,这就是所谓的迭代器失效.

总代码如下:

#include<iostream>
using namespace std;
namespace hmylq
{
	template<class T>
	class vector
	{
	public:
		//vector的迭代器是一个原生指针
		typedef T* iterator;
		typedef const T* const_iterator;

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finsh;
		}

		/Construct and destory//
		vector()
			:_start(nullptr)
			,_finsh(nullptr)
			,_end_of_storage(nullptr)
		{
			;
		}
		vector(int n, const T& val = T())//T()是一个匿名对象,如string,会调用它的构造函数,内置类型如int,也有构造函数,默认为0.
			:_start(nullptr)
			, _finsh(nullptr)
			, _end_of_storage(nullptr) 
		{
			reserve(n);
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}


		template<class Inputlterator>

		//迭代器区间构造
		vector(Inputlterator first, Inputlterator last)
			:_start(nullptr)
			,_finsh(nullptr)
			,_end_of_storage(nullptr)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}


		vector(const vector<T>& v)//拷贝构造
			:_start(nullptr)
			,_finsh(nullptr)
			,_end_of_storage(nullptr)
		{
			//_start = new T[v.size()];//v.capacity也可以
			//memcpy(_start, v._start, sizeof(T) * v.size());
			//_finsh = _start + v.size();
			//_end_of_storage = _start + v.size();
			reserve(v.size());
			for (const auto &e : v)
			{
				push_back(e);
			}
		}
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finsh, v._finsh);
			std::swap(_end_of_storage, v._end_of_storage);
		}

		//vector(const vector<T>& v)//拷贝构造
		//	:_start(nullptr)
		//	, _finsh(nullptr)
		//	, _end_of_storage(nullptr)
		//{
		//	vector<T> tmp(v.begin(), v.end());
		//	swap(tmp);
		//}

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

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

		///capacity///

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

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

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				if (_start)
				{
					memcpy(tmp, _start, sizeof(T) * sz);
					delete[] _start;
				}
 
				_start = tmp;
				_finsh = _start + sz ;
				_end_of_storage = _start + n;
			}
		}

		void resize(size_t n, const T& value = T())
		{
			if (n > capacity())
			{
				reserve(n);
			}
			if (n > size() )
			{
				while (_finsh < _start + n)
				{
					*_finsh = value;
					++_finsh;
				}
			}
			else
			{
				_finsh = _start + n;
			}
		}

		/access///

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

		/modify//	

		void push_back(const T& x)
		{
			if (_finsh == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}
			*_finsh = x;
			++_finsh;
		}


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

		void insert(iterator pos, const T& x) // 注意迭代器失效
		{
			assert(pos >= _start);
			assert(pos <= _finsh);

			if (_finsh == _end_of_storage)
			{
				size_t len = pos - _start;//先保存一下长度
				reserve(capacity() == 0 ? 4 : capacity() * 2);//如果需要扩容,pos位置则会改变,需要更新
				pos = _start + len;
			}

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




		void erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finsh);

			iterator begin = pos + 1;
			while (begin < _finsh)
			{
				*(begin - 1) == *begin; 
				++begin;
			}
			--_finsh;
		}
	private:
		iterator _start;//指向数据块的开始
		iterator _finsh;//指向有效数据的尾
		iterator _end_of_storage;//指向存储容量的尾
	};


}	

猜你喜欢

转载自blog.csdn.net/weixin_47257473/article/details/128670759