【STL】vector的模拟实现

目录

前言 

vector概述

vector的数据结构

vector迭代器的运用 

vector的构造和析构

vector的拷贝构造与赋值 

拷贝构造 

传统写法

现代写法

 赋值重载

vector的扩容 

reserve() 

 resize()

vector的元素操作

push_back() 

pop_back() 

insert()

erase() 

迭代器失效问题 

insert中迭代器失效问题

 erase中迭代器失效问题

完整代码链接


前言 

关于本篇可以先去看看上篇的string的模拟实现,更好的理解这里的内容。

关于vector容器的详细用法可以参考这个网站——vector

如果想更深的了解vector的底层实现的话可以看看侯捷老师写的《STL源码剖析》 

本文内容也参考了《STL源码剖析》中的内容

vector概述

vector是表示可变大小数组的序列式容器,就像数组一样,vector也采用了连续的空间来存储元素。这也就意味着可以采用下标对元素进行访问。vector是动态空间,随着元素的插入,它的内部机制会自动扩充空间来存储新数据,而且扩充的空间比实际存储的空间会更大,这是出于一种位于绸缪的考虑。这种有效的动态增长方式与其它动态序列式容器相比,vector在访问元素时更加高效。

vector的数据结构

vector的数据结构实现起来也非常简单:连续的线性空间。库里面的vector本身就是支持任何元素类型都可以用迭代器的方式进行访问,所以我们这里的实现的迭代器也要支持同样的操作——使用类模板,用两个迭代器start和finish分别指向分配来的连续空间中目前已经使用的范围,还有一个迭代器end_of_storage指向目前可用空间的尾。

vector迭代器的运用 

运用_start,_finish,_end_of_storage三个迭代器,便可轻易的提供首尾标示、大小、容量、 [](下标访问)、最前端元素和最后端元素

    typedef T* iterator;
	typedef const T* const_iterator;


	iterator begin()const
	{
		return _start;
	}

	iterator end()const
	{
		return _finish;
	}

	const_iterator cbegin()
	{
		return _start;
	}

	const_iterator cend()
	{
		return _finish;
	}

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

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

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

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

    T& front()
	{
		assert(size() > 0);
		return *_start;
	}

	T& back()
	{
		assert(size() > 0);
		return *(_finish - 1);
	}

vector的构造和析构

 vector的构造和析构是比较常规的操作了,也就是把上面的数据结构进行初始化和清理 

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

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

迭代器区间构造

	template <class InputIterator>
	MyVector(InputIterator first, InputIterator last)
		:_start(nullptr)
		, _finish(nullptr)
		, _end_of_storage(nullptr)
	{
		while (first != last)
		{
			push_back(*first);
			first++;
		}
	}

 这里使用的函数模板和上面的类模板是可以在一起使用的。

 指定大小初始化构造

    MyVector(size_t n, const T& value = T())
		:_start(nullptr)
		,_finish(nullptr)
		,_end_of_storage(nullptr)
	{
		reserve(n);
		while(n--)
		{
			push_back(value);
		}
	}

	MyVector(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);
		}
	}

 理论上提供了MyVector(size_t n, const T& value = T())之后,MyVector(int n, const T& value = T())接口可以不需要提供了,但是对于MyVector<int> v(10,5)这种,10和5都是int类型,编译器在编译时,会找和它最匹配的构造函数,n是size_t类型,T被示例化成了int类型,所以会选择MyVector(InputIterator first, InputIterator last),而不会选择MyVector(size_t n, const T& value = T()),因为编译器认为区间构造两个参数类型是一样的,所以会将InputIterator实例化成int,但是10和5根本不是一个区间,编译器编译时就会报错。因此需要增加这个接口。包括库里面也是这样实现的。

vector的拷贝构造与赋值 

拷贝构造和赋值如果我们不写编译器就会默认生成,而默认生成的是浅拷贝,在析构的时候就会出问题。

拷贝构造 

传统写法

    MyVector(const MyVector<T>& v)
	{
		_start = new T[v.size()];
		memcpy(_start, v._start, sizeof(T) * v.size());
		_finish = _start + v.size();
		_end_of_storage = _start + v.size();
	}

这种写法是大多数人都能想出来的,但是这里有个非常致命的问题就是使用了memcpy。

 

当你这样写的话,程序就会崩溃。 

memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中,如果拷贝的是内置类型的元素时,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型中涉及到资源管理时就会出错,因为memcpy实际上是一个浅拷贝。

 所以正确的写法是下面这样的

    //传统写法
	MyVector(const MyVector<T>& v)
	{
		_start = new T[v.size()];
		//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.size();
	}

但是有些人还会这样写,也是OK的

    MyVector(const MyVector<T>& v)
		:_start(nullptr)
		,_finish(nullptr)
		,_end_of_storage(nullptr)
	{
		reserve(v.size());
		for (const auto& d : v)
		{
			push_back(d);
		}
	}

现代写法

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

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

在现代写法中用到了一个迭代器区间的构造函数。而这里的tmp扮演的就是一个打工人的角色,勤勤恳恳的给老板(v)打工。

 赋值重载

 这里就直接上现代写法了,毕竟现代写法更简单

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

vector的扩容 

reserve() 

reserve实现起来也很简单,首先是判断空间够不够,如果不够就需要扩容,在扩容时需要开辟新空间,并把原来空间中的元素拷贝到新空间去,之后更新迭代器,所以我们很容易就会写出下面的代码。

	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;
			_finish = _start + sz;
			_end_of_storage = _start + n;
		}
	}

这样就和上面拷贝构造出现的问题一样了,对于自定义类型我们要完成的是一个深拷贝而不是浅拷贝。

正确写法 

    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);//浅拷贝
				for (size_t i = 0; i < sz; i++)
				{
					tmp[i] = _start[i];
				}
				delete[] _start;
			}

			_start = tmp;
			_finish = _start + sz;
			_end_of_storage = _start + n;
		}
	}

 resize()

 对于resize我们要考虑三种情况

  1. 当n > capacity()时,我们需要扩容+初始化
  2. 当n > size()并且n < capacity()时,我们只需要初始化
  3. 当n < size()时,我们需要删除多余的数据

    void resize(size_t n, const T& val = T())
	{
		if (n > capacity())
		{
			reserve(n);
		}
		if (n > size())
		{
			//初始化填值
			while (_finish < _start + n)
			{
				*_finish = val;
				_finish++;
			}
		}
		else
		{
			_finish = _start + n;
		}
	}

vector的元素操作

push_back() 

先判断空间够不够,如果不够就需要扩容,然后再将元素插入进去 

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

pop_back() 

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

insert()

在实现insert时,要注意扩容带来的迭代器失效问题,我们要保存一下pos位置,以免扩容时,pos迭代器失效。

之后往后挪动数据(从后往前挪动数据)将pos位置空出来,以便插入

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

		if (_finish == _end_of_storage)
		{
			size_t len = pos - _start;
			reserve(capacity() == 0 ? 4 : capacity() * 2);
			pos = _start + len;
		}

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

		*pos = x;
		_finish++;
		return pos;
	}

erase() 

和insert类似我们也需要挪动数据将pos位置的数据覆盖掉即可

 

	iterator erase(iterator pos)
	{
		assert(pos >= _start && pos < _finish);
		iterator begin = pos + 1;
		while (begin < _finish)
		{
			*(begin - 1) = *begin;
			begin++;
		}

		_finish--;
		return pos;
	}

迭代器失效问题 

insert中迭代器失效问题

在insert插入元素时,如果空间不够会发生扩容,扩完容后原来的空间就会释放掉,但是你的addr还是指向的是原来的空间,所以当你访问addr位置时,就会发生野指针问题。

所以当你用了一次addr最好就不要在用了

 erase中迭代器失效问题

 删除数据又是如何造成迭代器失效的呢?我们可以看一看下面的代码

下面这种情况是没有问题,正常运行的 

int main()
{
	MyVector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	//删除所有的偶数
	auto it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		it++;
	}
	for (auto v : v1)
	{
		cout << v << " ";
	}
	return 0;
}

 但是我们再增加一个数,它的结果就会出现问题了

int main()
{
	MyVector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	//删除所有的偶数
	auto it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		it++;
	}
	for (auto v : v1)
	{
		cout << v << " ";
	}
	return 0;
}

下面这个就会运行崩溃

int main()
{
	MyVector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	v1.push_back(6);

	//删除所有的偶数
	auto it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		it++;
	}
	for (auto v : v1)
	{
		cout << v << " ";
	}
	return 0;
}

这都是erase迭代器失效造成的,让我们来分析分析

 对于第二个代码

 这就是第二个结果导致的原因

对于第三个代码

在erase的实现中,我们是使用了在pos位置从前往后进行数据覆盖,并且erase返回的是当前删除数据的下一个位置,但是我们完成了一个数据覆盖,所以返回的还是当前删除数据的位置。

所以上面代码正确的写法是这样的

    auto it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		else
		{
			it++;
		}
	}

完整代码链接

 vector的模拟实现


 今天的分享就到这里了,如果内容有错的话还望指出,谢谢!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值