【C++】vector的模拟实现

前言

在前面我们已经介绍过了vector 的使用,为了加深我们对vector的理解,本篇文章我们来一起模拟实现vector 。关于本篇文章的相关代码,可以点击这里获取

vector的模拟实现

  • 一、vector的成员变量
  • 二、获取vector中相关信息的函数
    • 1、获取vector中元素的个数
    • 2、获取vector中容量的个数
    • 3、判断vector是否为空
  • 三、 构造函数、析构函数以及赋值重载函数
    • 1、无参的构造函数
    • 2、n个变量初始化的构造函数
    • 3、迭代器版本的构造函数
    • 4、拷贝构造函数
    • 5、析构函数
    • 6、赋值重载函数
  • 四、vector对象的访问 以及交换函数
    • 1、operator[]的函数重载
    • 2、迭代器的实现
    • 3、交换函数
  • 五、容量相关的函数
    • 1、reserve函数
    • 2、resize函数
  • 六、插入和删除相关的函数
    • 1、尾插函数
    • 2、尾删函数
    • 3、插入函数
    • 4、删除函数


一、vector的成员变量

在模拟实现一个类的时候,最重要的就是先确定这个类的成员变量,因为我们后面要实现的成员函数基本都是要操作成员变量的!那么对于vector我们的成员变量是什么呢? 按照vector本身的特性以及我们之前模拟实现string,你可能觉得这里的成员变量和string 一样是:

T* pT;
int size ;
int capacity;

确实,vector的结构与string很像,这样的成员变量模拟实现vector也没有什么问题,但是这里我们并不采取这种方式,我们采取SGI版本的vector写法。

  SGI版本的vector的成员变量是由三个T*类型的指针构成的,每个指针都有其各自的含义。

  • 第一个指针指向一块空间的起始位置。
  • 第二个指针指向空间中最后一个元素的下一个位置。
  • 第三个指针是指向整个空间结束位置的下一个位置
//指向一块空间的起始位置
T* _start;

//指向空间中最后一个元素的下一个位置
T* _finish;

//指向整个空间结束位置的下一个位置
T* _end_of_storage;

【C++】vector的模拟实现_第1张图片

这些是我们将要实现的函数:

template<class T>
class vector
{
public:
	
	//获取容量
	size_t capacity() const;
	//获取元素个数
	size_t size() const;
	//判空函数
	bool empty() const;


	//无参构造函数
	vector();
	
	//n个变量初始化的构造函数 ,这里缺省值是一个匿名对象
	vector(size_t n, const T& val = T());
	vector(int n, const T& val = T());

	//迭代器初始化vector对象 
	template<class InputIterator>
	vector(InputIterator first, InputIterator last);

	//拷贝构造
	vector(const vector<T>& val);

	//赋值重载  
	vector<T>& operator=(vector<T> val);

	//析构函数
	~vector();
	
	//[]运算符重载
	//普通版本
	T& operator[](size_t n);
	//const版本
	const T& operator[](size_t) const;


	//普通迭代器  
	typedef  T* iterator;
	iterator begin();
	iterator end();
	
	//const迭代器
	typedef const T* const_iterator;
	const_iterator begin() const;
	const_iterator end() const;
	
	//交换函数
	void swap(vector<T>& val);

	//容量相关的函数     
	//reserve函数
	void reserve(size_t n);
	//resize函数, 这里缺省值是一个匿名对象
	void resize(size_t n, const T& val = T());


	//modify        
	//尾插函数
	void push_back(const T& val);
	//尾删函数
	void pop_back();
	//插入函数
	iterator insert(iterator pos, const T& val);
	//删除函数
	iterator erase(iterator pos);
	
private:
	T* _start = nullptr;
	T* _finish = nullptr;
	T* _end_of_storage =nullptr;
};

注意这里我们给了成员变量缺省值nullptr

二、获取vector中相关信息的函数

1、获取vector中元素的个数

由于我们的成员变量是T*的指针,两个指针相减得到的是这两个指针之间相差的本类型指针个数。这个个数刚好等于vector中元素的个数,因此我们可以这样实现:

template<class T>
size_t vector<T>::size() const
{
	return size_t(_finish - _start);
}

2、获取vector中容量的个数

同理,我们可以使用两个指针相减得到容量的个数。

template<class T>
size_t vector<T>::capacity() const
{
	return size_t(_end_of_storage - _start);
}

3、判断vector是否为空

由于我们是用指针实现的,每个指针都有特定的含义,根据指针的含义,这里我们只需要判断_finish指针与_start指针是否相等,如果相等说明vector里面一个元素也没有。

template<class T>
bool vector<T>::empty() const
{
	return (_finish == _start);
}

三、 构造函数、析构函数以及赋值重载函数

1、无参的构造函数

对于无参的构造函数,我们只需要对该对象内部的成员变量完成初始化就行了。对于指针的初始化,我们一般将指针初始化为nullptr,我们也知道对于普通成员变量我们是可以给缺省值进行初始化的,因此我们可以给成员变量缺省值nullptr,然后我们无参的构造函数什么也不做。

template<class T>
vector<T>::vector()
{
}

2、n个变量初始化的构造函数

对于n个参数的构造函数,我们可以先开辟一块能存储n个元素空间,然后将我们想要初始化的值一个一个赋值给新空间就行了,最后再更新一下该对象里面指针的指向就行了。

template<class T>
vector<T>::vector(size_t n, const T& val)
{
	//开辟空间
	T* tmp = new T[n];
	for (size_t i = 0; i < n; ++i)
	{
		//调用拷贝构造挨个赋值
		tmp[i] = val;
	}
	//更新对象里面指针的指向
	_start = tmp;
	_finish = tmp + n;
	_end_of_storage = tmp + n;
}

当然为了方便我们的使用以及避免与后面的迭代器版本的构造函数 发生冲突(当T是int类型,我们这样调用vector v(2,7)构造函数,会调用到迭代器版本,与我们想调用的函数不符),我们还可以再重载一个int版本的该函数。

template<class T>
vector<T>::vector(int n, const T& val)
{
	T* tmp = new T[n];
	for (int i = 0; i < n; ++i)
	{
		//调用拷贝构造挨个赋值
		tmp[i] = val;
	}
	_start = tmp;
	_finish = tmp + n;
	_end_of_storage = tmp + n;
}

3、迭代器版本的构造函数

迭代器版本的构造函数,我们可以使用函数模板来进行实现,这样不仅可以用vector 的迭代器来初始化,也可以使用其他的迭代器来初始化!

  • 这里其他的迭代器,例如string,普通数组名 ,都可以,只要能够隐式类型转换成功就行。

具体实现我们可以和n个变量初始化的构造函数的做法一致,先开空间然后再进行赋值,只不过赋值的时候我们需要先解引用一下。

template<class T>
template<class InputIterator>
vector<T>::vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		//这里的push_back()函数是尾插函数,我们还没有实现,具体实现在下面
		push_back(*first);
		++first;
	}
}

4、拷贝构造函数

拷贝构造我们还是先开空间然后再进行逐一赋值就行了。

template<class T>
vector<T>::vector(const vector<T>& val)
{
	size_t sz = val.size();
	size_t n = val.capacity();
	T* tmp = new T[n];
	for (size_t i = 0; i < sz; ++i)
	{
		tmp[i] = val[i];
	}
	_start = tmp;
	_finish = tmp + sz;
	_end_of_storage = tmp + n;
}

5、析构函数

对于析构函数我们只需要释放掉_start所指向的空间,然后逐一对对象里面的三个指针赋值为nullptr就行了。

template<class T>
vector<T>::~vector()
{
	delete[] _start;
	_start = nullptr;
	_finish = nullptr;
	_end_of_storage = nullptr;
}

6、赋值重载函数

对于赋值重载函数,这里我们写一种现代版本的写法。

我们可以给赋值重载函数的参数采用传值传参,传值传参需要拷贝构造一个新对象给赋值重载函数,这个拷贝出来的新对象不就是我们赋值重载的调用对象想要的吗?于是我们可以使用交换函数,将新对象的内容交换给我们的赋值重载的调用对象。这样我们也是完成了赋值操作。

【C++】vector的模拟实现_第2张图片

template<class T>
vector<T>& vector<T>::operator=(vector<T> val)
{
	//交换函数,我们后面实现
	swap(val);
	return *this;
}

四、vector对象的访问 以及交换函数

string对象的访问主要有两种:[]访问, 迭代器访问。(范围for的本质还是迭代器)

1、operator[]的函数重载

对于operator[n],我们应该返回vectorn位置的引用,我们只需要将_start指针向后移动 n个单位然后解引用返回就行了。

//[]运算符重载  普通版本
template<class T>
T& vector<T>::operator[](size_t n)
{
	//检查n的位置是否合法
	assert(n < size());
	return _start[n];
}
//[]运算符重载  const版本
template<class T>
const T& vector<T>::operator[](size_t n) const
{
	//检查n的位置是否合法
	assert(n < size());
	return _start[n];
}

2、迭代器的实现

对于vector迭代器的实现,我们可以使用原生指针去当作迭代器。
在类中我们可以使用typedef T类型的指针来得到迭代器类型。

//普通迭代器
typedef  T* iterator;

//const版本的迭代器
typedef const T* const_iterator;

由于我们的成员变量是三个指针,所以对于begin() end()函数,我们实现起来也并不复杂,仔细思考就会发现begin() end()就是我们的_start指针与_finish指针。


这里唯一要注意的就是:我们在类外实现时,函数的返回值前要多加一个typename来帮助编译器识辨iteratorvector类内的一个typedef的一个泛型类型。

//普通迭代器
template<class T>
typename vector<T>::iterator vector<T>::begin()
{
	return _start; 
}
template<class T>
typename vector<T>::iterator vector<T>::end()
{
	return _finish;
}


//const迭代器
template<class T>
typename vector<T>::const_iterator vector<T>::begin() const
{
	return _start;
}
template<class T>
typename vector<T>::const_iterator vector<T>::end() const
{
	return _finish;
}

3、交换函数

我们知道C++的算法库< algorithm> 中提供了一个交换函数,但是那个交换函数是使用借助第三个变量进行交换的 ,对于内置类型这样交换并不影响程序的效率,但是对于自定义类型这样的交换就会大大影响程序的效率,所以vector中提供了一个更高效率的交换函数。

【C++】vector的模拟实现_第3张图片

我们可以通过交换指针的指向来使对象完成交换,这样我们就不必创建第三个变量进行交换了。

【C++】vector的模拟实现_第4张图片

【C++】vector的模拟实现_第5张图片

template<class T>
void vector<T>::swap(vector<T>& val)
{
	//使用std标准库里面的交换函数完成内置类型的交换
	std::swap(_start, val._start);
	std::swap(_finish, val._finish);
	std::swap(_end_of_storage, val._end_of_storage);
}

五、容量相关的函数

与容量相关的函数主要有两个:分别是resreve resize函数,其中resize函数可以复用reserve函数,因此我们先来实现reserve函数。

1、reserve函数

当给的容量值n小于原有的容量时,我们什么也不做,当给的容量值n大于我们原有的容量时,我们才进行真正的扩容,扩容时的操作其实也是先开辟新空间,然后拷贝原有的数据,再之后释放旧空间,最后更新对象里面的指针指向。

template<class T>
void vector<T>::reserve(size_t n)
{
	// n小于原有的容量时,我们什么也不做
	if (n > capacity())
	{
		//先开辟新空间
		T* tmp = new T[n];
		size_t sz = size();
		//拷贝原有的数据
		for (int i = 0; i < sz; ++i)
		{
			tmp[i] = _start[i];
		}
		//不要忘记释放旧空间,否则会造成内存泄漏
		delete[] _start;
		//更新对象里面的指针指向
		_start = tmp;
		_finish = tmp + sz;
		_end_of_storage = tmp + n;
	}
}

2、resize函数

  • n < capacity()时,resize函数不会进行缩容,只会减小_finish指针的值。
  • size() < n < capacity()时,resize函数只会对size()后面的元素进行初始化(或赋值),并不会进行扩容。
  • n > capacity()时,resize函数会进行扩容与初始化(或赋值)
template<class T>
void vector<T>::resize(size_t n, const T& val)
{
	if (n < size())
	{
		//只减小_finish指针指向的位置
		_finish = _start + n;
	}
	else
	{
		//先判断是否需要进行扩容
		if (n > capacity())
		{
			reserve(n);
		}
		//进行size()后面的元素的初始化(或赋值)
		for (int i = size(); i < n; ++i)
		{
			_start[i] = val;
		}
		_finish = _start + n;
	}
}

六、插入和删除相关的函数

1、尾插函数

对于插入相关的操作我们在进行之前都要先判断:空间是否是足够的,如果是足够的,我们就说明也不做,如果是不够的,那我们就需要进行扩容了!

前面的扩容的判断步骤做完了以后,我们就可以通过对对象里面的_finish指针进行解引用然后进行赋值,最后对_finish进行自增一下就行了。

template<class T>
void vector<T>::push_back(const T& val)
{
	//判断空间是否足够
	if (_finish == _end_of_storage)
	{
		//注意扩容时,capacity不能等于 0 ,0时要做特殊处理
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}
	//解引用然后进行赋值,最后对_finish进行自增
	*_finish = val;
	++_finish;
}

2、尾删函数

对于删除相关的操作我们在删除元素时都要先判断该空间上的元素个数是否是0,如果是空还进行删除就是不合理的操作了。

如果上面的检查通过以后,我们就要进行删除元素了,但是我们没有必要进行真正的删除,我们只需要让_finish指针自减一下就行了。

template<class T>
void vector<T>::pop_back()
{
	//检查当前的vector中元素的个数是0
	assert(!empty());
	--_finish;
}

3、插入函数

对于插入函数的插入操作,我们可以通过检查迭代器的位置是否是合法的来判断要不要进行插入操作。

对于插入相关的操作我们还是要判断:空间是否是足够的,如果是足够的,我们就说明也不做,如果是不够的,那我们就需要进行扩容了!

前面的扩容的判断步骤做完了以后,我们就要进行真正的插入了,这里的插入也很简单,我们将待插入位置后面的元素一 一向后进行移动为待插入的元素腾出空间,然后直接进行插入就行了。

这里要注意的就是:我们在类外实现时,函数的返回值前要多加一个typename来帮助编译器识辨iteratorvector类内的一个typedef的一个泛型类型。

template<class T>
typename vector<T>::iterator vector<T>::insert(typename vector<T>::iterator pos, const T& val)
{
	//检查插入的位置是否合法
	assert(pos >= _start);
	assert(pos <= _end_of_storage);
	//判断是否需要扩容
	if (_finish == _end_of_storage)
	{
		//扩容时要注意迭代器失效的问题
		int len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;
	}
	//移动数据
	iterator end = _finish;
	while (pos < end)
	{
		*end = *(end - 1);
		--end;
	}
	//插入
	*pos = val;
	//更新指针的位置
	++_finish;
	//返回一个迭代器的值,外面接收的话可以避免insert函数内部扩容而导致外面的迭代器失效。
	return pos;
}

插入函数的实现里面细节还是挺多的,要多注意其中的细节。

4、删除函数

对于删除函数的删除操作,我们可以通过检查迭代器的位置是否是合法的来判断要不要进行删除操作。

我们进行删除操作,可以通过数据向前移动进行覆盖的方式进行删除,最后更新一下_finish指针就行了。

这里要注意的就是:我们在类外实现时,函数的返回值前要多加一个typename来帮助编译器识辨iteratorvector类内的一个typedef的一个泛型类型。

template<class T>
	typename vector<T>::iterator vector<T>::erase(typename vector<T>::iterator pos)
	{
		//检查迭代器的位置是否是合法
		assert(pos >= _start);
		assert(pos < _end_of_storage);
		//数据向前移动进行覆盖的方式进行删除
		iterator end = pos + 1;
		while (end < _finish)
		{
			*(end - 1) = *end;
			++end;
		}
		//更新_finish指针的位置
		--_finish;
		//返回一个迭代器的值,指向删除位置,有需要可以在外面接收一下。
		return pos;
	}

你可能感兴趣的:(C++,c++,数据结构)