[C++进阶(四)] STL之Vector的模拟实现

💓博主CSDN主页::Am心若依旧💓

⏩专栏分类c++从入门到精通
🚚代码仓库:青酒余成🚚

🌹关注我🫵带你学习更多c++
  🔝🔝

 文章目录

一、Vector模拟实现的整体框架

Vector在功能上就是在学习数据结构阶段实现的顺序表基本一致,但是Vector在成员框架上与顺序表有所不同,且Vector使用类和对象封装支持模板泛型。

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

private:
	iterator _start = nullptr;//存储首元素的地址
	iterator _finish = nullptr;//存储有效元素的下一个地址
	iterator _end_od_storage = nullptr;//存储最大容量所在位置的地址
};

成员变量如下图所示: 

vector的迭代器就是顺序表原生类型的指针

二、要实现的各类函数的接口


	//模拟实现vector
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		//默认成员函数
		vector();                                           //构造函数
		vector(size_t n, const T& val);                     //构造函数
		template<class InputIterator>                      
		vector(InputIterator first, InputIterator last);    //构造函数
		vector(const vector<T>& v);                         //拷贝构造函数
		vector<T>& operator=(const vector<T>& v);           //赋值运算符重载函数
		~vector();                                          //析构函数

		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin()const;
		const_iterator end()const;

		//容量和大小相关函数
		size_t size()const;
		size_t capacity()const;
		void reserve(size_t n);
		void resize(size_t n, const T& val = T());
		bool empty()const;

		//修改容器内容相关函数
		void push_back(const T& x);
		void pop_back();
		void insert(iterator pos, const T& x);
		iterator erase(iterator pos);
		void swap(vector<T>& v);

		//访问容器相关函数
		T& operator[](size_t i);
		const T& operator[](size_t i)const;

	private:
		iterator _start;        //指向容器的头
		iterator _finish;       //指向有效数据的尾
		iterator _endofstorage; //指向容器的尾
	};

三、默认成员函数

构造函数

构造函数1:vector首先支持一个无参的构造函数,对于这个无参的构造函数,我们直接将构造对象的三个成员变量都设置为空指针即可

//构造函数1
Vector()
	:_start(nullptr)
	,_finish(nullptr)
	,_end_of_storage(nullptr)
{}

构造函数2:vector也支持使用一段迭代器区间进行对象的构造。因为该迭代器区间可以是其他容器的迭代器区间,也就是说该函数接收到的迭代器的类型是不确定的,所以我们这里需要将该构造函数设计为一个函数模板,在函数体内将该迭代器区间的数据一个个尾插到容器当中即可。这里只有满足输入型迭代器的才可以使用该模板进行构造

//构造函数2
template <class InputIterator>
Vector(InputIterator first, InputIterator last)
	: _start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

构造函数3:vector还支持构造这样一种容器,该容器当中含有n个值为val的数据。对于该构造函数,我们可以先使用reserve函数将容器容量先设置为n,然后使用push_back函数尾插n个值为val的数据到容器当中即可。

//构造函数3--在数组里面插入n个val
Vector(size_t n, const T& val) 
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	reserve(n);//先开辟空间,在插入--这样可以避免在push_back里面在进行扩容了
	for (int i = 0; i < n; i++)
	{
		push_back(val);
	}
}

析构函数

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

只需要释放空间即可。这里有个小细节,那就是防止释放掉本来就没有的空间,所以加了一个判断。

拷贝构造函数

拷贝构造函数涉及到深浅拷贝的问题,并且有两种写法。下面先给出传统的写法

传统写法的过程:先开辟一块与该容器大小相同的空间,然后将该容器当中的数据一个个拷贝过来即可,最后更新_finish和_endofstorage的值即可 

Vector(const Vector<T>& v)
	:_start(nullptr)
	,_finish(nullptr)
	,_end_of_storage(nullptr)
{
	_start = new T[v.capacity()];
	for (int i = 0; i < v.size(); i++)
	{
		//这里不能用memcpy,这就涉及到深浅拷贝的问题了。
       	_start[i] = v[i];
	}
	_finish = _start+v.size();
	_end_of_storage = _start+v.capacity();
}

 注意:

将容器当中的数据一个个拷贝过来时不能使用memcpy函数,当vector存储的数据是内置类型或无需进行深拷贝的自定义类型时,使用memcpy函数是没什么问题的,但当vector存储的数据是需要进行深拷贝的自定义类型时,使用memcpy函数的弊端就体现出来了。例如,当vector存储的数据是string类的时候。

 并且vector当中存储的每一个string都指向自己所存储的字符串。

 如果此时我们使用的是memcpy函数进行拷贝构造的话,那么拷贝构造出来的vector当中存储的每个string的成员变量的值,将与被拷贝的vector当中存储的每个string的成员变量的值相同,即两个vector当中的每个对应的string成员都指向同一个字符串空间。

这显然不是我们得到的结果,那么所给代码是如何解决这个问题的呢?

 

 代码中看似是使用普通的“=”将容器当中的数据一个个拷贝过来,实际上是调用了所存元素的赋值运算符重载函数,而string类的赋值运算符重载函数就是深拷贝,所以拷贝结果是这样的:

 

 总结一下: 如果vector当中存储的元素类型是内置类型(int)或浅拷贝的自定义类型(Date),使用memcpy函数进行进行拷贝构造是没问题的,但如果vector当中存储的元素类型是深拷贝的自定义类型(string),则使用memcpy函数将不能达到我们想要的效果。

其实最好的不管什么类型你都使用深拷贝,那么不管遇到什么类型的拷贝,都不会出现问题

拷贝构造函数现代写法:

 其实这个写起来也比较简单,先创建一个临时变量,然后再把这个临时变量与要拷贝构造的值进行互换,那么这样的话就相当于把两个人掉了个位置,临时变量出了作用域也被销毁了,又能够取得交换的效果。

void Swap(Vector& tmp)
{
	std::swap(_start, tmp._start);
	std::swap(_finish, tmp._finish);
	std::swap(_end_of_storage, tmp._end_of_storage);
}
Vector(const Vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	Vector tmp(v.begin(), v.end());
	Swap(tmp);
}

赋值重载函数

赋值重载函数也有两个写法,先给出传统的写法

传统写法:先判断是否是自己给自己赋值,如果不是,就把空间释放掉,然后再重新开辟一块空间,再把值拷贝过来。

//Vector<T>& operator=(const Vector<T>& v)
//	:_start(nullptr)
//	,_finish(nullptr)
//	,_end_of_storage(nullptr)
//{
//	//先判断是否给自己赋值
//	if (this != &v)
//	{
//		delete[]_start;
//		for (int i = 0; i < v.size(); i++)
//		{
//			_start[i] = v[i];
//		}
//	}
//	return *this;
//}

现代写法:和拷贝构造函数一样,先创建临时变量,然后再交换,直接就能够完成赋值重载函数

Vector<T>& operator=(const Vector<T>& v)
	: _start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	Vector tmp(v.begin(), v.end());
	Swap(tmp);
	return *this;
}

迭代器相关的函数

vector当中的迭代器实际上就是容器当中所存储数据类型的指针。

typedef T* iterator;
typedef const T* const_iterator;

 begin和end

 vector当中的begin函数返回容器的首地址,end函数返回容器当中有效数据的下一个数据的地址。

	iterator begin()
	{
		return _start;
	}
	iterator end()
	{
		return _finish;
	}

 容量和大小相关的函数

 size和capacity

size表示有效数据的个数,capacity表示容量的大小

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

 empty

 empty主要是判断该段空间里面是否有数据

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

 resize和reserve函数

reserve函数:

reserve规则:
 1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
 2、当n小于对象当前的capacity时,什么也不做。

 reserve实现起来也很简单:先判断当前n是否大于当前的容量,大于的话,就把原来的有效的数据先记录下来,然后再创建一个容量为n的临时空间,把有效的值赋给这个临时空间里面的值,然后在释放原来的空间,然后再把这个临时空间赋给_start

void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t sz = size();
		T* tmp = new T[n1];
		//扩容的原则是,如果原来有就释放原来的空间,然后开辟一块新空间,再把数据拷贝过来
		if (_start)
		{
			for (int i = 0; i < sz; i++)
			{
				tmp[i] = _start[i];
			}
			delete[]_start;
		}
		_start = tmp;
		_finish = _start + sz;
		_end_of_storage = _start + n;
	}
}

1)在进行操作之前需要提前记录当前容器当中有效数据的个数。
因为我们最后需要更新_finish指针的指向,而_finish指针的指向就等于_start指针加容器当中有效数据的个数,当_start指针的指向改变后我们再调用size函数通过_finish - _start计算出的有效数据的个数就是一个随机值了。

 resize函数

resize规则:
 1、当n大于当前的size时,将size扩大到n,扩大的数据为val,若val未给出,则默认为容器所存储类型的默认构造函数所构造出来的值。
 2、当n小于当前的size时,将size缩小到n。

 根据resize函数的规则,进入函数我们可以先判断所给n是否小于容器当前的size,若小于,则通过改变_finish的指向,直接将容器的size缩小到n即可,否则先判断该容器是否需要增容,然后再将扩大的数据赋值为val即可。

void resize(size_t n, const T& val=T())
{
	if (n < size())
	{
		_finish = _start + n;
	}
	else
	{
		//n>=size()--先判断是否需要扩容
		if (n > capacity())
		{
			reserve(n);
		}
		while (_finish < _start + n)
		{
			*finish = val;
			finish++;
		}
	}
}

 ps: 在C++当中内置类型也可以看作是一个类,它们也有自己的默认构造函数,所以在给resize函数的参数val设置缺省值时,设置为T( )即可。

 增删查改相关函数

 pop_back和push_back

在进行尾插前要先判断是否需要扩容,如果不需要就直接插入,否则就先扩容在插入

void push_back(const T& val)
{
	//判断是否需要扩容
	if (_finish == _end_of_storage) //判断是否需要增容
	{
		size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //将容量扩大为原来的两倍
		reserve(newcapacity); //增容
	}
	*_finish = x; //尾插数据
	_finish++; //_finish指针后移
}

 尾删的话,只要元素不为空,那么就可以直接删

void pop_back()
{
	assert(!empty());
	_finish--;
}

 insert和erase函数

 insert函数可以在所给迭代器pos位置插入数据,在插入数据前先判断是否需要增容,然后将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入,最后将数据插入到pos位置即可。

void insert(iterator pos, const T& val)
{
	//判断是否需要扩容
	if (_finish == _end_of_storage)
	{
		size_t len = pos-_start;//记录要插入的位置
		size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //将容量扩大为原来的两倍
		reserve(newcapacity); //增容
		pos = _start + len; //通过len找到pos在增容后的容器当中的位置
	}
	//把pos位置的数据整体向后移
	iterator end = _finish;
	while (end > pos)
	{
		*end = *(end - 1);
		end--;
	}
	*pos = val;
	_finish++;
}

 ps:这里在扩容之前一定要记录要插入位置的值,如果不记录的话,当你扩容之后,pos位置就已经失效了。那就会造成非法访问

 erase函数,由于删除函数可能在删除某一个位置的值之后,那么这个位置的值就会发生失效,失效之后再次访问就造成了随机访问。所以说在删除一个迭代器位置的值之后,在重新给他返回一个有效且合法的迭代器的位置

erase函数也很好写,就是把pos位置后面的值往前移动,覆盖pos位置的值就可以了。

iterator erase(iterator pos)
{
	assert(!empty());
	//把pos后面的值往前移动
	iterator end = pos + 1;
	while (end != _finish)
	{
		*(end - 1) = *end;
		end++;
	}
	_finish--;
	return pos;
}

 下标访问容器函数

 operator[]重载

 vector也支持我们使用“下标+[ ]”的方式对容器当中的数据进行访问,实现时直接返回对应位置的数据即可。

	T& operator[](size_t pos)
	{
		assert(pos < size());//首先访问的数据要合法
		return _start[pos];
	}

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

好啦,行文至此vector大部分关键的函数就已经全部模拟实现完成了,希望各位小伙伴也能够自己去手动尝试。 

 

                                            下期预告:list的介绍和相关使用

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值