【C++】 vector的模拟实现

一.物理框架

通过上次vector的使用的学习
我们知道vector也是顺序存储,即使用连续的地址空间存储数据,设计原理同string,但vector是通过迭代器表示开始,结束,容量

在这里插入图片描述

以下是SGI的STL的部分源码

在这里插入图片描述

在这里插入图片描述

vector使用类模板,可接收,适配不同数据类型,同样适配自定义类型
iterator是封装的接收的数据的指针

二.成员变量

对标STL标准库,我们也使用这样的指针,但仅简单实现就好

template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

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

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

	private:
		T*_start;
		T*_finish;
		T*_end_of_storage;
	};

typedef T*iterator要定义在public中,因为类外需要直接使用迭代器
_start,_finish,_end_of_storage均定义在类内
注意:因为存在const引用传参,所以还需要const的迭代器,并且相关函数还需要重载const this指针

三.数据存取

因为是顺序存储,所以在我们插入数据时,仍需考虑是否需要扩容。
而扩容则需要获取当前的大小和容量:通过迭代器区间获取

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

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

注意:size和capacity需要外加const,因为若是const T&x,x需要调用这两个函数,就会因为权限扩大而无法使用

  • 接下来是扩容函数–reserve
		//扩容
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				T*tmp = new T[n];
				size_t sz = size();
				//原来有数据才拷贝
				if (_start)
				{
					memcpy(tmp, _start, sizeof(T)*size());
					delete[] _start;
				}

				_start = tmp;
				//不可以_finish=_start+size();
				//因为_start已经指向新空间,_finish还指向旧空间
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}
		}

注意:不可以_finish=_start+size();
因为_start已经指向新空间,_finish还指向旧空间
size的_finish-_start已不是原来的连续空间的大小了

在这里插入图片描述

  • 尾插–push_back较为简单,直接上代码
		void push_back(const T&x)
		{
			//满了需要扩容
			if (_finish == _end_of_storage)
			{
				//不可直接reserve(capacity()*2);
				//因为capacity()最初是0
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}

			*_finish = x;
			++_finish;
		}

因为是自定义类型,所以数据的访问需要重载[ ]
同时还有const对象

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

		const T& operator[](size_t i) const
		{
			assert(i < size());

			return _start[i];
		}
  • 测试
	void vector_Test1()
	{
		vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);

		for (size_t i = 0; i < v1.size(); i++)
		{
			cout << v1[i] << " ";
		}
		cout << endl;
	}

在这里插入图片描述

  • 访问我们同样还可以使用迭代器和更为简洁的范围for
    同样也要适配const对象
		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

	void vector_Test1()
	{
		vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);

		vector<int>::iterator it = v1.begin();
		while (it != v1.end())
		{
			cout << *it << " ";
			it++;
		}
		cout << endl;

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

在这里插入图片描述

  • resize调整大小
		//调整大小
		void resize(size_t n,T val=T())
		{
			if (n < size())
			{
				//删除数据
				_finish = _start + n;
			}
			else
			{
				//扩容
				if (n > capacity())
				{
					reserve(n);
				}
				//初始化
				while (_finish != _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}
  • pop_back删除

因为内部是指针,所以删除需要判断是否为空,为空则直接报错

		bool empty()
		{
			return _start == _finish;
		}

		void pop_back()
		{
			assert(!empty());

			--_finish;
		}
  • insert–指定位置插入
		void insert(iterator pos, const T&val)
		{
			assert(pos >= _start);
			assert(pos <= _finish);

			if (_finish == _end_of_storage)
			{
				//记录pos的相对l位置
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : 2 * capacity());
				//更新pos位置,防止pos迭代器失效
				pos = _start + len;
			}

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

			*pos = val;
			++_finish;
		}

迭代器失效

注意:因为可能会扩容,一旦扩容,_start等迭代器会更新,但是pos仍指向原来的区域,会出现pos迭代器失效的问题,直接像string那样移动数据会有无法预料的错误
所以,我们需要记录原pos相对_start的位置,然后再扩容后,更新pos指向的位置
在这里插入图片描述

但是
在这里插入图片描述
vector的insert是有支持返回值是迭代器的版本的?

这是什么意思呢

小场景

	void vector_Test3()
	{
		vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);

		fun(v1);

		v1.insert(v1.begin(), 30);

		auto pos = find(v1.begin(), v1.end(), 2);
		if (pos != v1.end())
		{
			v1.insert(pos, 666);
		}

		//如果我们还要再外部修改pos指向的值呢?
		(*pos)++;
		
		fun(v1);
	}

如果我们还要修改在外部的pos位置呢
那当然是不行的,因为如果发生扩容,外部的pos仍指向原来空间。函数内部的pos因为是值传递,形参改变不会影响实参。
在这里插入图片描述
那我们能在参数列表里修改为引用吗?
答案也是不行的

在这里插入图片描述
在这里插入图片描述

因为有这样的插入方式,而begin是传值返回,是创建临时对象,有常量性,不能用引用接收

所以有返回iterator的函数类型
所以完整insert代码如下

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

			if (_finish == _end_of_storage)
			{
				//记录pos位置
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : 2 * capacity());
				//更新pos位置,防止pos迭代器失效
				pos = _start + len;
			}

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

			*pos = val;
			++_finish;

			return pos;
		}
  • erase
		void erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);

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

			_finish--;
		}

erase理论上不会出现迭代器失效的情况,因为删除并不会改变存储地址
但是vs的pj版的STL内部实现和我们实现的并不相同,并且有严格的检查(为了防止越界访问,如删除最后一个数据,但却还通过迭代器访问)
g++内部是原生指针,并不会报错

小场景

比如我们再vector内插入1,2,3,4,5然后删除其中的偶数

我们先用std库里的看看结果

首先是VS下

	void vector_Test4()
	{
		std::vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);


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

		std::vector<int>::iterator it = v1.begin();
		while (it != v1.end())
		{
			if (*it % 2 == 0)
			{
				v1.erase(it);
			}
			else
			{
				it++;
			}
		}

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

在这里插入图片描述

理论而言,如果是我们自己的实现的话,应该没有问题,但是VS进行了强制的检查,认为只要调用了erase,相应的迭代器就失效

Linux下
在这里插入图片描述
在这里插入图片描述
删除成功了
因为g++所采用的SGI版本的STL,内部实现迭代器也是使用原生指针,所以检查不严格。
VS的强制检查会更合理,因为我们可能会进行尾删,那么迭代器指向的就是尾删的数据,按理来说,我们不应该还能访问到删除的数据,但是erase后迭代器依旧使用,我们就可以访问。所以VS的强制检查会更加合理

四.构造

在这里插入图片描述
PS: explict是不允许隐式类型转换

函数定义函数说明
vector(size_type n,const value_type&val=value_type())n个val初始化
vector(InputIterator first,InputItreator last)迭代器区间初始化
vector(const vector& x)拷贝构造

n个val初始化

匿名对象

匿名对象生命周期只在当前一行,因为这行之后没有会用他了。

class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}
};

int main()
{
	A a1;
	cout << endl;
	A();
	cout << endl;
	A a2;
	
	return 0;
}

在这里插入图片描述

但const引用会延长匿名对象的生命周期,延长到引用对象域的结束,因为以后用引用就是用匿名对象了

int main()
{
	A a1;
	cout << endl;
	
	const A&a = A();
	
	cout << endl;
	A a2;


	return 0;
}

在这里插入图片描述

实现

		//n个val初始化
		//使用匿名对象作缺省参数
		vector(size_t n, const T&val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			reverse(n);
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

迭代器区间

为了适配各种数据类型的迭代器,所以迭代器区间的初始化需要使用函数模板。
类模板允许内部继续使用函数模板

		//迭代器区间初始化
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}

		}

小优化

我们发现有参构造,n个val构造,迭代器区间构造,我们是不是都使用了初始化列表
那么我们可以使用C++11的新语法简化我们的代码。
因为_start,_finish,_end_of_storage都是内置类型,所以我们可以在定义的时候给值,该值系统会在初始化列表使用

private:
		T*_start=nullptr;
		T*_finish=nullptr;
		T*_end_of_storage=nullptr;

小问题

当我们测试上述两个构造时,我们发现报错了
在这里插入图片描述
并且报错位置在迭代器区间处
在这里插入图片描述
这是为什么呢?

原来,我们查看n个val初始化的参数列表,第一个参数的类型是size_t的,而迭代器区间是两个相同的参数,那么我们传过去的10,5都是 int,会优先匹配迭代器区间构造的函数,然后内部的*first就报错了,因为匹配后InputItera是int类型,int类型怎么能解引用呢?

SGI的STL的解决办法就是再重载n个val构造函数

		//int的重载
		vector(int n, const T&val = T())
		{
			reserve(n);
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

在这里插入图片描述

测试

	void vector_Test5()
	{
		//10个5构造
		vector<int>v1(10, 5);
		for (auto e : v1)
		{
			cout << e << " ";
		}
		cout << endl;

		//迭代器区间构造
		vector<int>v2(v1.begin()+1, v1.end()-1);
		for (auto e : v2)
		{
			cout << e << " ";
		}
		cout << endl;

		//string的迭代器构造
		std::string s1("hello");
		vector<int>v3(s1.begin(), s1.end());
		for (auto e : v3)
		{
			cout << e << " ";
		}
		cout << endl;

		//数组的指针构造
		int a[] = { 10,20,30 };
		vector<int>v4(a, a + 3);
		for (auto e : v4)
		{
			cout << e << " ";
		}
		cout << endl;
	}

在这里插入图片描述

五.构造函数–深拷贝

我们首先试着写深拷贝并测试

  • 代码
		//拷贝构造--深拷贝
		vector(const vector<T>&v)
		{
			reserve(v.capacity());
			memcpy(_start, v._start, sizeof(T)*v.size());
			_finish = _start + v.size();
		}
  • 测试
	void vector_Test6()
	{
		vector<int>v1(10, 5);
		for (auto e : v1)
		{
			cout << e << " ";
		}
		cout << endl;
		vector<int>v2(v1);
		for (auto e : v2)
		{
			cout << e << " ";
		}
		cout << endl;

	}
  • 运行结果

在这里插入图片描述

看样子,好像没问题,也就这样,但问题真的解决了吗?
我们不妨再测试一下
我们使用string的vector

int main()
{
	string s1("hello");
	vector<string>v3(3, s1);
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;

	vector<string>v4(v3);
	for (auto e : v4)
	{
		cout << e << " ";
	}
	cout << endl;
}

在这里插入图片描述
报错了。
为什么呢?
通过调试,我们发现是在析构函数程序崩溃的。这时我们发现,还是深浅拷贝的问题,对相同空间重复释放了
在这里插入图片描述
虽然vector的_start所指向的空间是独立的,但是因为memcpy是按字节拷贝,内部的string的_str指向还是同样的空间。这样在程序结束时,会先调用string的析构函数,再调用vector的析构函数,那这样string的空间重复释放,就报错了

那我们使用operator=赋值构造,因为string的operator=是深拷贝,这样string所指空间就不会相同了

		//拷贝构造--深拷贝
		vector(const vector<T>&v)
		{
			reserve(v.capacity());
			//memcpy(_start, v._start, sizeof(T)*v.size());
			for (int i = 0; i < v.size(); i++)
			{
				_start[i] = v._start[i];
			}

			_finish = _start + v.size();
		}

在这里插入图片描述
但是,回看我们写的扩容的函数,其内部是不是也使用了memcpy,那么扩容时也会造成浅拷贝,而重复析构空间崩溃
所以我们进行以下调整

//扩容
void reserve(int n)
{
	if (n > capacity())
	{
		T*tmp = new T[n];
		size_t sz = size();
		//原来有数据才拷贝
		if (_start)
		{
			//memcpy(tmp, _start, sizeof(T)*size());
			for (size_t i = 0; i < sz; i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
		}

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

但是其实问题还没有完全解决,已经解决了99%了

剩下的问题是vector内部套用vector

int main()
{
	vector<int>v1(3, 3);
	vector<vector<int> >vv1(3,v1);
	for (auto e : vv1)
	{
		for (auto x : e)
		{
			cout << x << " ";
		}
		cout << endl;
	}
	cout << endl;
	return 0;
}

在即将return 0结束程序时,vv1的值成功被打印出来了,但是一return 0调用析构函数,程序又崩溃了? 因为上述的operator=解决了其他STL的问题,因为他们内部提供了深拷贝的opeartor=,但是我们自己写的vector还没有编写深拷贝的operator=,所以系统调用默认的operator=,其是浅拷贝
所以我们接下来完成一下operator=的编写

operator=

//拷贝构造——深拷贝
vector(const vector<T>&v)
{
	reserve(v.capacity());
	//memcpy(_start, v._start, sizeof(T)*v.size());
	for (int i = 0; i < v.size(); i++)
	{
		_start[i] = v._start[i];
	}

	_finish = _start + v.size();
}

//swap封装函数
void swap(vector<T> v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_end_of_storage, v._end_of_storage);
}

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

我们要拷贝vector<vector< int> >,首先外层的vector,是深拷贝,是新开辟的空间,然后_start[ i ]实际指向内部的vector< int>,此时,我们调用operator=,此时opeartor的参数是传值,会调用拷贝构造,但是是vector< int>的深拷贝,因为其调用的拷贝构造内部的_start指向的int,直接浅拷贝值,vector< int>是新开辟的空间,然后operator=内部再调用swap,将传值拷贝构造的新空间和我们要深拷贝的vector的指针交换,就实现深拷贝了

以此基础,我们可以再将拷贝构造改造一下。我们复用迭代器区间构造

//拷贝构造--深拷贝
vector(const vector<T>&v)
{
	vector<T>tmp(v.begin(), v.end());
	swap(tmp)
}

完整代码

template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		vector()
		{}

		//n个val初始化
		vector(size_t n, const T&val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

		//int类型的重载
		vector(int n, const T&val = T())
		{
			reserve(n);
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

		//迭代器区间初始化
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}

		}

		//拷贝构造--深拷贝
		vector(const vector<T>&v)
		{
			vector<T>tmp(v.begin(), v.end());
			swap(tmp)
		}


		//swap封装函数
		void swap(vector<T> v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}

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

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

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

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

		bool empty()
		{
			return _start == _finish;
		}

		void pop_back()
		{
			assert(!empty());

			--_finish;
		}


		void push_back(const T&x)
		{
			//满了需要扩容
			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}

			*_finish = x;
			++_finish;
		}

		//扩容
		void reserve(int n)
		{
			if (n > capacity())
			{
				T*tmp = new T[n];
				size_t sz = size();
				//原来有数据才拷贝
				if (_start)
				{
					for (size_t i = 0; i < sz; i++)
					{
						tmp[i] = _start[i];
					}
					delete[] _start;
				}

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

		//调整大小
		void resize(size_t n,T val=T())
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else
			{
				if (n > capacity())
				{
					reserve(n);
				}

				while (_finish != _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}

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

			if (_finish == _end_of_storage)
			{
				//记录pos位置
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : 2 * capacity());
				//更新pos位置,防止pos迭代器失效
				pos = _start + len;
			}

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

			*pos = val;
			++_finish;

			return pos;
		}

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

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

			_finish--;

			return pos;
		}

		T& operator[](int i)
		{
			assert(i < size());

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

			return _start[i];
		}

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

	private:
		T*_start=nullptr;
		T*_finish=nullptr;
		T*_end_of_storage=nullptr;
	};

结束语

vector的模拟实现基本结束了。
文章如果有不对或者不足的地方,欢迎大佬们指正,补充。感谢大家的阅读,如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下,阿里嘎多。拜托了,这对我真的很重要~
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值