Vector的模拟实现与迭代器失效问题

一.Vector的介绍与使用

vector的英文解释是向量的意思,向量是序列容器,表示大小可以变化的数组。就像数组一样是连续存储的可以通过指针偏移来访问元素。但与数组不同的是它的大小可以动态变化,其存储由容器自动处理。在存储数据时容器可能会分配一些额外的空间来适应可能的增长,因此实际的容器可能大于包含其元素严格需要的存储空间。

二.Vector的实现

接下来我会通过vector的基本功能用法,迭代器的模拟,以及构造析构方面进行模拟实现。

首先搭好架子,创建一个类模板T,将T的指针定义为iterator后续用来模拟实现迭代器,_start表示指向首位的指针,_finish表示指向有效元素末尾的指针,_end_of_storage表示容器最大存储量。

cpp 复制代码
namespace bicup
{
	template<class T>//类型模板
	class vector     //类型
	{
	public:
		typedef T* iterator;//类型指针

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

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

1.构造析构函数

1.构造与析构

下面书写一下构造函数和析构函数,当然这里的构造函数我们先写一部分,后面根据需要来进行补充修改。

cpp 复制代码
namespace bicup
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		vector()
		:_start(nullptr)
		,_finish(nullptr)
		,_end_of_storage(nullptr)
		{}

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

需要注意这里的构造在 :下要用()而不能用 = 直接初始化 ,析构时需要判断vector内是否有数据。

2.拷贝函数

下面是拷贝构造

cpp 复制代码
namespace bicup
{
	template<class T>//类型模板
	class vector     //类型
	{
	public:
		typedef T* iterator;//类型指针
		typedef const T* const_iterator;

		vector(const vector<T>& v)
		{
			reserve(v.size());
			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=(const vector<T>& v)
		{
			swap(v);
			return *this;
		}*/

		vector<T>& operator=(const vector<T>& v)
		{
			if (this != &v)
			{
				delete[] _start;
				_start = _finish = _end_of_storage = nullptr;
				reserve(v.size());
				for (auto& e : v)
				{
					push_back(e);
				}
			}
			return *this;
		}

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

这里有两种表示方法,v2(v1)或者v2 = v1,此处没有太多的难点。

2.功能函数模拟

下面来实现尾插,首先对vector进行判断,是否有空间插入数据,若没有则需要进行扩容。在扩容这部分操作我们需要用到reserve以及vector空间大小capacity和实际大小size,我们在下面依次实现。为了节省空间以及更加清楚的展示,每个板块只放上核心代码部分,当然在最后我会将所有代码一次性放上。

1.reverse与push_back

cpp 复制代码
#pragma once

namespace bicup
{
	template<class T>
	class vector
	{
	public:

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

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

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

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

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

reverse部分中,需要判断扩容是否大于capacity, 若小于capacity则不会改变容量,若大于capacity我们不能在原有的空间上进行添加数据。我们需要重新的开辟一块新的空间,new出n个单位的空间,通过memcpy将原有的数据转移到新开辟的空间上,回收原有的内存空间。将tmp首元素地址给_start,更新_finish和_end_of_storage。

当然这块并不是最终的代码,下面我们再来看一个例子

cpp 复制代码
int main()
{
	bicup::vector<string> v1;
	v1.push_back("1111111111111111");
	v1.push_back("111111111111111111");
	v1.push_back("1111111111111111111");
	v1.push_back("111111111111111111");
	v1.push_back("11111111111111111111");

	for (const auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

当我们运行这串代码后发现

这是什么原因导致的呢?

这里使用了一个memcpy,它属于浅拷贝,它通过首元素的地址对整个string进行拷贝,所以当vector扩容之后tmp会与_start指向同一个位置,最后会被delete[ ] 析构掉。所以看到的就是乱码。

那我们该怎么调整呢?

很简单,不使用memcpy就可以了

cpp 复制代码
namespace bicup
{
	template<class T>//类型模板
	class vector     //类型
	{
	public:
		typedef T* iterator;//类型指针
		typedef const T* const_iterator;

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t oldsize = size();
				T* tmp = new T[n];
				//memcpy(tmp, _start, sizeof(T) * oldsize);
				for (size_t i = 0; i < oldsize; i++)
				{
					tmp[i] = _start[i];
				}
				delete[] _start;
				_start = tmp;
				_finish = _start + oldsize;
				_end_of_storage = _start + n;
			}
		}

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

2. [ ] 运算符重载

接下来测试一下push_back,我们希望像数组一样用 [ ] 的方式来表示下标元素,这就需要用到运算符重载。

cpp 复制代码
#pragma once
#include <string>
#include <assert.h>

namespace bicup
{
	template<class T>//类型模板
	class vector     //类型
	{
	public:
		typedef T* iterator;//类型指针

    	T& operator[](size_t i)
		{
			assert(i < size());
			return _start[i];//等于*(_start + i)
		}

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

这里的本质就是以指针_start为起始点+i个单位之后解引用得到的数据。 可以用_start [ i ] 来简便书写。

测试代码:

cpp 复制代码
int main()
{
	bicup::vector<int> vv;
	vv.push_back(2);
	vv.push_back(3);
	vv.push_back(5);

	for (const auto &e : vv)
	{
		cout << e << " ";
	}
	cout << endl;

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

3.迭代器模拟

当我们需要测试const类型的vector时,原先的下标以及迭代器就用不了了,需要重新进行函数重载。

例如下面的测试代码:

cpp 复制代码
void func(const bicup::vector<int>& v)
{
	for (size_t i = 1; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;
	bicup::vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
}

当然上述的代码是跑不起来的,原因是const进行了函数的权限缩小,但是我们还没有进行函数重载。这里需要注意,我们不能直接在 bicup::vector<int>::iterator it = v.begin() 前面加const,因为这里加的const就是修饰迭代器本身不能达到我们的目的,我们需要const来修饰的是vector里的数据。所以请看

cpp 复制代码
namespace bicup
{
	template<class T>//类型模板
	class vector     //类型
	{
	public:
		typedef T* iterator;//类型指针
		typedef const T* const_iterator;

		iterator begin()
		{
			return _start;
		}

		const_iterator begin() const
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator end() const
		{
			return _finish;
		}

		T& operator[](size_t i)
		{
			assert(i < size());
			return _start[i];//等于*(_start + i)
		}

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


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

我们需要在typedef处添加const修饰,随之而来的我们需要重载一份const的函数begin() end() 以及 [ ] 。

下面是更正后的写法:

cpp 复制代码
void func(const bicup::vector<int>& v)
{
	for (size_t i = 1; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;
	bicup::vector<int>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
}

三.迭代器失效问题

1.野指针

接下来是本篇的重点部分,关于迭代器失效的问题。我们先来看一串代码。

cpp 复制代码
namespace bicup
{
	template<class T>//类型模板
	class vector     //类型
	{
	public:
		typedef T* iterator;//类型指针
		typedef const T* const_iterator;

		void insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : 2 * capacity());
			}
			iterator it = _finish - 1;
			while (it >= pos)
			{
				*(it + 1) = *it;
				it--;
			}
			*pos = x;
			_finish++;
		}

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

我们来看insert这部分逻辑,空间不够扩容,移动数据,插入数据,更新_finish 好像没有什么问题。事实也是如此,当我们用5个数据测试时并没有找出问题,但当我们用4个数据测试时,程序就崩溃了。原因是vector内只有4个数据时要插入数据,那么就会涉及到扩容,扩容的时候需要将数据移动到另一块空间上。但是此时vector内的 _start 仍然指向原空间首位,所以需要我们对其进行更新。

正确写法:

cpp 复制代码
#pragma once
#include <string>
#include <assert.h>

namespace bicup
{
	template<class T>//类型模板
	class vector     //类型
	{
	public:
		typedef T* iterator;//类型指针
		typedef const T* const_iterator;

		void 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 : 2 * capacity());
				_start = len;
			}
			iterator it = _finish - 1;
			while (it >= pos)
			{
				*(it + 1) = *it;
				it--;
			}
			*pos = x;
			_finish++;
		}


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

2.编译器强制检查

当然迭代器失效的情况不止这一种,下面我们来继续探讨。

cpp 复制代码
int main()
{
	bicup::vector<int> vv;
	vv.push_back(2);
	vv.push_back(3);
	vv.push_back(5);
	vv.push_back(4);

	int i;
	cin >> i;
	auto it = vv.begin() + i;
	vv.insert(it, 2);
	cout << *it << " ";

	return 0;
}

当我们创建一个变量 it 来传参时,函数的形参会改变但是实参并没有进行更新,所以 it 无法找到扩容之后的pos位置。有的同学可能会疑惑,那不能在函数参数上添加 & 吗?当然这也是不行的,虽然添加之后 it 会进行更新,但参数一旦添加常量之后(it + 1)就无法运行通过了。所以,规定了只要使用了insert函数,迭代器就会失效(不使用)。
接下来我们看下一个经典的迭代器失效。

cpp 复制代码
int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	//删除偶数
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			v.erase(it);
		}
		it++;
	}
}

这个操作是对偶数项进行删除,但是当我们在vs下编译时确没有结果显示。

这是由于编译器的设定 ,在vs下当编译器当程序erase之后,认为迭代器失效会将其标记,一旦访问就会报错。

总结,迭代器的失效主要是由两种原因导致,一个是野指针另一个是强制检查标记。虽然不同编译器对于erase和insert的迭代器处理不同,但我们默认使用了erase和insert后迭代器失效。

四.完整代码

cpp 复制代码
#pragma once
#include <string>
#include <assert.h>

namespace bicup
{
	template<class T>//类型模板
	class vector     //类型
	{
	public:
		typedef T* iterator;//类型指针
		typedef const T* const_iterator;

		iterator begin()
		{
			return _start;
		}

		const_iterator begin() const
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator end() const
		{
			return _finish;
		}

		T& operator[](size_t i)
		{
			assert(i < size());
			return _start[i];//等于*(_start + i)
		}

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

		vector()
		{}

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

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

		void resize(size_t n, const T& val = T())
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else
			{
				reserve(n);
				while (_finish != _start + n)
				{
					*_finish = val;
					_finish++;
				}
			}
		}

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t oldsize = size();
				T* tmp = new T[n];
				//memcpy(tmp, _start, sizeof(T) * oldsize);
				for (size_t i = 0; i < oldsize; i++)
				{
					tmp[i] = _start[i];
				}
				delete[] _start;
				_start = tmp;
				_finish = _start + oldsize;
				_end_of_storage = _start + n;
			}
		}

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

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

		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 : 2 * capacity());
				_start = len;
			}
			iterator it = _finish - 1;
			while (it >= pos)
			{
				*(it + 1) = *it;
				it--;
			}
			*pos = x;
			_finish++;
			return pos;
		}

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

		vector(const vector<T>& v)
		{
			reserve(v.size());
			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=(const vector<T>& v)
		{
			swap(v);
			return *this;
		}*/

		vector<T>& operator=(const vector<T>& v)
		{
			if (this != &v)
			{
				delete[] _start;
				_start = _finish = _end_of_storage = nullptr;
				reserve(v.size());
				for (auto& e : v)
				{
					push_back(e);
				}
			}
			return *this;
		}

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

五.结语

创作不易,希望大家多多支持!!!

相关推荐
qq_433618442 小时前
c++ chrono 操作 以及c语言的时间操作
开发语言·c++
BuluAI2 小时前
解密AIGC三大核心算法:GAN、Transformer、Diffusion Models原理与应用
算法·生成对抗网络·aigc
想不明白的过度思考者3 小时前
关于扫雷的自动补空实现C语言
c语言·算法·游戏
CodeCraft Studio3 小时前
「实战应用」如何为DHTMLX JavaScript 甘特图添加进度线
javascript·算法·甘特图
wclass-zhengge3 小时前
02UML图(D1_结构图)
java·开发语言·算法
近听水无声4773 小时前
STL容器-- list的模拟实现(附源码)
c++·学习
孑么3 小时前
力扣 打家劫舍
java·算法·leetcode·职场和发展·动态规划
九离十4 小时前
基于C语言的通讯录实现
c语言·开发语言
mljy.5 小时前
优选算法《二分查找》
c++·算法