[STL]详解vector模拟实现

[STL]vector模拟实现

1. 整体结构总览

template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		vector(); //默认构造函数
		vector(size_t n, const T& val = T());
		vector(int n, const T& val = T());
		template <class InputIterator>
		vector(InputIterator first, InputIterator last); //迭代器初始化
		vector(const vector<T>& v);//拷贝构造
		~vector();//析构函数
		
		iterator begin();//返回指向数据开头的迭代器
		iterator end(); //返回指向最后一个数据的下一个数据的位置的迭代器
		const_iterator begin() const;//const迭代器版本
		const_iterator end() const;//const迭代器版本

		size_t size()const; //获取容器内有效数据的个数
		size_t capacity()const;//获取容器容量
		void reserve(size_t n); //扩容函数
		void resize(size_t n, T val = T());//扩容函数
		bool empty();//检查容器是否为空

		void push_back(const T& x); //尾插函数
		void pop_back();//尾删函数
		iterator insert(iterator pos, const T& val);//插入函数
		iterator erase(iterator pos);//删除函数

		T& operator[](size_t n); //[]重载
		const T& operator[](size_t n) const; //const变量版本[]重载
		vector<T>& operator=(const vector<T>& v);//传统写法
		//vector<T>& operator=(vector<T> v);//现代写法

		void swap(vector<T>& v);//交换函数

	private:
		iterator _start;  //指向数据开头的位置
		iterator _finish; //指向最后一个有效数据的下一个数据的位置
		iterator _end_of_storage; //指向容量空间的末尾位置
	};

2. 成员变量解析

_start 指向数据的开始位置,_finish指向最后一个有效数据的后一个数据的位置,_end_of_storage指向容量空间的末尾位置。3.

3. 默认成员函数

构造函数1

首先是无参数的默认的构造函数,构造的是一个空容器,因此成员变量赋空值就行。

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

构造函数2

此构造函数实现的功能是在定义时开一定空间,并且将这段空间全是相同的数据,由于模拟实现这个构造函数复用了其他函数,要注意一定要初始化变量,否则会影响正常复用其他函数。

vector(size_t n, const T& val = T())//const 引用可以延长匿名对象的寿命,使该匿名对象的寿命和引用相当
	: _start(nullptr), 
	_finish(nullptr),
	_end_of_storage(nullptr)
{
	reserve(n);//提前扩容,提高效率
	for (size_t i = 0; i < n; ++i)
	{
		push_back(val);
	}
}
vector(int n, const T& val = T())//重载一个版本,兼容更多类型
	: _start(nullptr), 
	_finish(nullptr),
	_end_of_storage(nullptr)
{
	reserve(n);
	for (size_t i = 0; i < n; ++i)
	{
		push_back(val);
	}
}

构造函数3

此构造函数的功能是利用不同类型迭代器初始化容器,只要迭代器指向数据和容器数据类型能匹配上就能使用该函数,包括指向数组的原生指针可看作迭代器,可以作为参数传入。

template <class InputIterator>
vector(InputIterator first, InputIterator last)//指向数组的原生指针可看作迭代器,可以作为参数传入
	: _start(nullptr),
	_finish(nullptr),
	_end_of_storage(nullptr)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

拷贝构造函数

拷贝构造函数的实现需要注意深浅拷贝的问题,首先要注意自身空间需要深拷贝,拷贝构造构造容器时为自身需要申请独立的空间,然后将数据进行拷贝,而不是将相应成员变量(_start等成员变量)直接赋值,两个容器指向同一块空间,造成问题。

image-20230616211124792

其次要注意的是如果容器存储的是需要申请空间的自定义类型,在拷贝构造时,也需要单独为它们申请空间然后拷贝数据,而不是将要拷贝的容器的数据直接赋值过来,比如存储string类,直接赋值会导致两个容器存储的string对象指向相同的空间。

image-20230616212300898

传统写法:

申请新的空间然后将数据一个一个拷贝过来。

vector(const vector<T>& v)
{
	//传统写法
	_start = new T[v.capacity()]; //为容器申请独立空间
	for (size_t i = 0; i < v.size(); ++i)
	{
		_start[i] = v._start[i];//如果是内置类型直接赋值,如果是自定义类型,自定义类型会重载=运算符,满足深拷贝的需求,比如string类的=运算符就是对数据进行深拷贝
	}
	_finish = _start + v.size();
	_end_of_storage = _start + v.capacity();
}

现代写法:

创建一个新的容器拷贝数据,然后将该容器的空间转让过来。

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(const vector<T>& v)
	: _start(nullptr),
	_finish(nullptr),
	_end_of_storage(nullptr)
{
	//现代写法
	vector<T> tmp(v.begin(), v.end());//用迭代器初始化创建一个和v数据相同的vector
	swap(tmp);
}

另外值得注意的是由于是利用自定义类型会重载=运算符,满足深拷贝的需求,因此需要实现=运算符重载,并且是对数据深拷贝,否则使用vector<vector<T>>类型时也会因为浅拷贝出现问题。

析构函数

由于空间是new出来的连续空间,因此只需要用delete关键字释放就可以,然后将各成员变量置空。

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

4. 迭代器相关函数

begin函数

begin函数的功能是返回指向数据开始位置的迭代器,因此只需要返回_start变量就可以。

iterator begin()
{
	return _start;
}

end函数

end函数的功能是返回指向最后一个有效数据的下一个数据的位置,因此只需要返回_finish变量就可以。

iterator end()
{
	return _finish;
}

begin函数const版本

const版本的begin函数是提供给const类型的容器使用,因此this指针加const才能适配,并且返回值得是const版本的迭代器。

const_iterator begin() const
{
	return _start;
}

end函数const版本

和const版本的begin函数同理,this指针加const,并且返回值是const版本的迭代器。

const_iterator end() const
{
	return _finish;
}

5.容量相关函数

size函数

由于申请的空间是连续的,因此只需要用_finish减_start就可以得到容器内有效数据的个数。

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

capacity函数

和size函数原理相同,只需要用_end_of_storage减_start即可获取容器的容量。

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

reserve函数

reserve规则

1. 如果n小于capacity,什么都不做。
1. 如果n大于capacity,就将capacity扩容到n。
void reserve(size_t n)
{
	if (n > capacity())
		{
			T* tmp = new T[n]; //申请能存储n个T类型数据的空间
			size_t sz = size();//提前保存原有size大小,后续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;//指向新空间的末尾位置
		}
}

注意:原有有效数据个数需在_start指向修改前保存,否则由于_start指向的改变导致size函数的结果为错误的,进而导致_finish指向的位置不正确。

image-20230615151119279

resize函数

resize规则:

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

void (size_t n, T val = T())
{
	if (n < size())//n是无符号整形,不用担心n为负数造成_finish指向位置为非法位置
	{
		_finish = _start + n;
	}
	else
	{
		if (n > capacity())//n大于capacity需要扩容
		{
			reserve(n);
		}
        while (_finish != _start + n)//将原有有效数据位置到第n个数据位置设置为val
		{
			*_finish = val;
			++_finish;
		}
	}
}

注意: 由于需要兼容模板, 因此在C++当中内置类型也可以看作是一个类,它们也有自己的默认构造函数,所以在给resize函数的参数val设置缺省值时,设置为T( )即可。

empty函数

empty函数的作用是检查容器是否为空,当_start和_finish指向的位置相同时就是空。

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

image-20230615153442627

6. 数据修改函数

push_back函数

容器已满就先扩容再尾插,容器未满就直接尾插,由于_finish指向的是最后一个有效数据的下一个数据的位置,因此只需要对其解引用就可以尾插数据。

void push_back(const T& x)
{
	if (_finish == _end_of_storage) //容器已满,需要扩容
	{
		size_t newcapacity = capacity == 0 ? 4 : capacity() * 2; //扩容空间至原有空间的2倍
		reserve(newcapacity);
	}
	//容器未满,直接尾插
	*_finish = x;
	++_finish;
}

pop_back函数

删除数据前要保证有数据,因此断言判断容器不为空,删除数据只需要将_finish的指向改变就可以。

void vector<T>::pop_back()
{
	assert(!empty());//检查是否有数据
	--_finish;
}

image-20230615154321112

insert函数

insert函数的功能是将数据插入到迭代器指向的位置,因此迭代器的指向的正确性是很重要的。首先要断言判断插入位置是否越界;其次要注意容器是否有空间插入数据,因为如果空间不够,需要利用申请新的空间的方法来实现扩容,申请新的空间,会导致形参传入的指向插入数据位置的迭代器失效,具体讲就是迭代器指向原有空间,而原有空间已经释放了,导致迭代器失效,因此在容量不足时要单独判断,并且保证迭代器指向正确的位置才能完成正确插入的。由于空间不论怎么变,指向的数据和数据开头的偏移量是不变的,因此需要利用偏移量来保证迭代器指向位置的正确即可,保证迭代器指向正确后,只需要挪动数据,将要插入的数据写入正确位置即可。返回插入位置的迭代器。

image-20230615205803223

iterator insert(iterator pos, const T& val)
{
	assert(pos >= _start);//检查插入位置是否越界
	assert(pos <= _finish);
	size_t len = pos - _start;//记录偏移量
	if (_finish == _start)//容器已满
	{
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
		pos = _start + len;//使得pos指向新空间的相应位置
	}
	iterator end = _finish - 1;
	while (pos <= end)
	{
		*(end + 1) = *end;
		--end;
	}
	*pos = val;
	++_finish;
	return pos;
}

erase函数

删除数据首先要确保删除的数据是有效数据,因此断言判断迭代器指向的位置,然后只需要将删除数据后的数据向前挪动覆盖即可,最后别忘了将指向最后一个有效数据的下一个位置的_finish修正。

iterator erase(iterator pos)
{
	assert(pos >= _start);//判断删除位置是否越界
	assert(pos < _finish);
	iterator start = pos + 1;
	while (start != _finish)
	{
		*(start - 1) = *start;
		++start;
	}
	--_finish;
	return pos;
}

7. 数据访问函数

[]运算符重载

申请的是连续的空间,因此只需要利用偏移量找到对应的位置解引用即可,由于[]需要支持修改数据内容,返回类型必须是引用。

T& operator[](size_t n)
{
    assert(n < size());//检查是否越界访问
	return _start[n];
}

[]运算符重载const版本

const版本的[]运算符重载是提供给const类型的容器使用,因此this指针加const才能适配,并且返回值得是const类型的数据。

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

=运算符重载const版本

传统写法:

申请相应大小的新的空间然后将数据依次拷贝。

vector<T>& operator=(const vector<T>& v)
{
	if (_start != v._start) //避免v1 = v1出错
	{
		delete[] _start;
		_start = new T[v.capacity()];
		for (size_t i = 0; i < v.size(); ++i)
		{
			_start[i] = v._start[i];
		}
		_finish = _start + v.size();
		_end_of_storage = _start + v.capacity();
	}
	return *this;
}

现代写法:

利用形参会拷贝构造实参,然后将形参的空间交换过来。

vector<T>&operator=(vector<T> v)
{
	//现代写法
	swap(v);
	return *this;
}

8. 完整源码链接

STL/vector/vector/vector.h · 钱雪明/日常代码 - 码云 - 开源中国 (gitee.com)

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

好想写博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值