STL的容器vector的模拟实现

1.vector的成员变量的介绍

cpp 复制代码
namespace xiaoli
{
	template<class T>
	class vector
	{
	public:
	private:
		T* _start = nullptr;
		T* _finish = nullptr;
		T* _end_of_storage = nullptr;
	};
}

_start:指向容器起始位置;

_finish:指向有效元素的下一位置;

_end_of_storage:指向容器空间的尾;

2. 默认成员变量

2.1 构造函数

(1)构造函数1:无参构造

cpp 复制代码
vector()
{

}

(2)构造函数2:指定空间大小

给定空间大小n,如果n<size(),那么移动_finish=_start+n的位置;

如果n>size(),说明空间大小不足,需要扩容reserve,再按照给定的内容修改;

cpp 复制代码
vector(size_t n, const T& val)
{
	if (n < size())
	{
		_finish = _start + n;
	}
	else
	{
		//扩容
		reserve(n);
		while (_finish != _start + n)
		{
			*_finish = val;
			++_finish;
		}
	}
}

说明:这里可以进一步完善,上面的代码先判断空间大小是否足够,在进行数据填充;

那么是否捡现成吃?-- 啥意思呢? 在模拟实现过程会发现这段代码和resize(n)的实现是一样的,直接复用resize()进行完善,代码会更加简洁!

cpp 复制代码
vector(size_t n, const T& val)
{
	resize(n, val);
}

(3)构造函数3:迭代器构造

根据参数first和last,进行比较,如果不等说明是有效的迭代器,复用push_back可以插入数据;

注:因为push_back内部实现是判断了是否需要扩容的问题!!

那么插入的位置就是first的位置,插入之后,first往后移动即可;

cpp 复制代码
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

注意:这里存在频繁扩容的问题!!

解决办法就是:提前预留好指定大小空间。

(4)构造函数4:初始化列表

传统写法:开辟等大空间,在finish指定位置插入数据;

cpp 复制代码
vector(initializer_list<T> il)
{
	//step1:先判断空间够不够
	reserve(il.size());
	for (auto& e : il)
	{
		if (_finish == _end_of_storage)
		{
			size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
			reserve(newcapacity);
		}
		*_finish = e;
		_finish++;
	}
}

优化:提前开辟好空间(是为了防止push_back频繁的扩容),复用push_back,还解决了扩容问题;

cpp 复制代码
vector(initializer_list<T> il)
{
	// 开辟等大的空间
	reserve(il.size());
	for (auto& e : il)
	{
		push_back(e);
	}
}

2.2 析构函数

cpp 复制代码
~vector()
{
    if(_start) //空指针不用处理
    {
	    delete[] _star;
	    _start = _finish = _end_of_storage = nulptr;
    }
}

注意,如果vector里面的内容是自定义类型,在vector析构的时候会调用对应析构函数!

2.3 拷贝构造

传统写法:

开辟等大空间,挨个拷贝内容

cpp 复制代码
vector(vector<T>& v)
{
	//_start = _finish = _end_of_storage = nullptr;
	reserve(v.capacity());
	for (size_t i = 0; i < v.size(); i++)
	{
		_start[i] = v._start[i];
	}
	_finish = _start + v.size();
}

一个一个拷贝有点太复杂了,欸C语言之前不是有一个拷贝函数memcpy,这样不仅就简化了代码吗?事实上这个真的能行吗?我们先来试试,遇到问题在分析!

于是就写出了这样的代码:

cpp 复制代码
vector(vector<T>& v)
{
	reserve(v.capacity());
	memcpy(tmp, v._start, , sizeof(T) * v.size());
	_finish = _start + v.size();
	_end_of_storage = _start + v.capacity();
}

问题:这个代码一运行哪哪都报错!为什么?

我的老大哥啊,首先_start是私有成员,你怎么能访问?

其次,memcpy是浅拷贝,如果拷贝完后,你的v给释放掉了,那么你的v2也会受到影响。

如果是内置类型影响可能没有很明显,但是如果是自定义类型就会有很大隐患!!!这里就假设vector里面存储自定义类型!!!

当你的v释放掉之后,v2的_start也会随之变化。

如何解决?既然已经知道是浅拷贝的问题,那么深拷贝就能处理(即传统写法/现代写法)

**总结一下:**如果是浅拷贝就可以使用memcpy,但是如果是深拷贝,不能使用否则就会出问题!!

现代写法:

cpp 复制代码
vector(vector<T>& v)
{
	//_start = _finish = _end_of_storage = nullptr;
	reserve(v.capacity());
	for (auto& e : v)
	{
		push_back(e);//优化
	}	
}

现代写法:直接套用push_back方法,同时提前预开辟了空间,不会频繁扩容!!!

2.4 赋值运算符

传统写法:

step1:开辟同样大小的空间;step2:遍历原vector数组,进行赋值;step3:更新私有成员。

cpp 复制代码
//传统写法
vector<T>& operator=(vector<T>& v)
{
	// 清理
	delete[] _start;
	_start = _finish = _end_of_storage = nullptr;
	reserev(v.capacity());
	for (size_t i = 0; i < v.size(); i++)
	{
		_start[i] = v._start[i];
	}
    _finish=_start+v.size();
    _endofstorage = _start + v.capacity(); 
	return *this;
}

现代写法:

你不是想要和我一样,那我用临时对象接受你,在和临时对象进行数据交换不就好了?

实现交换由两个办法:

方法1:创建临时对象,构造一份,在和我交换;

方法2:传值构造,在和形参进行交换;(这里可不是拷贝构造,不要求是引用传参)

cpp 复制代码
iterator operator=(vector<T> tmp)
{
	swap(tmp);
	return _start;
}

3. 容量和大小相关的函数

3.1 size和capacity

只需要知道size:有效元素的个数;capacity:空间的大小。

cpp 复制代码
size_t capacity() const
{
	return _end_of_storage - _start;
}

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

3.2 reserve

首先要判断空间是否足够,不够就扩容并更新内容和私有成员;

cpp 复制代码
void reserve(size_t n)
{
	if (n > capacity())
	{
		//扩容
		T* tmp = new T[n];
		// 拷贝
		for (size_t i = 0; i < size(); i++)
		{
			tmp[i] = _start[i];
		}
		delete[] _start;
		//error
		//_start = tmp;
		// 注意这里的_start已经更新,但是_finish还是空,就有问题
		//所以得先更新_finish
		_finish = tmp + size();
		_start = tmp;
		_end_of_storage = _start + n;
	}
}

但是这里需要注意两个问题:

问题1:当拷贝完内容,释放_start时,如果直接写_finish=_start+size()会报错,原因如下:

size()是_finish - _start,但此时_finish还是空的,_start已经更新,那么size()就会报错;

所以需要先更新_finish(目的是为了size()不报错),再去更新_start.

问题2:memcpy代替拷贝,会出现浅拷贝的问题

这里观看监视窗口,可以发现他们的地址是一样的,当_start资源释放的时候,tmp也会销毁;

注意:vector里面的资源是string,但是当释放资源时,string会调用它的析构函数,所以string也会被销毁。这里还是使用赋值拷贝,是因为string的赋值拷贝时深拷贝!!!

所以要注意memcpy的使用场景!!!

3.3 resize

当n<capacity()时,一般不做处理或者删除有效元素;

当n>capacity()时,扩容/扩容并填充内容。

cpp 复制代码
void resize(size_t n, T val = T())
{
	if (n < capacity())
	{
		_finish = _start + n;
	}
	else
	{
		reserve(n);
		while (_finish != _start + n)
		{
			*_finish = val;
			_finish++;
		}
	}
}

3.4 empty

empty是检测是否为空,那就是判断有没有有效元素,即判断_start==_finish?

cpp 复制代码
bool empty()
{
	if (_start == _finish)
		return true;
	else
		return false;
}

4. 修改容器内容相关的函数

4.1 push_back

同样的先判断空间是否足够,在进行数据插入。

cpp 复制代码
void push_back(const T& val)
{
	// 先判断空间大小
	if (_finish == _end_of_storage)
	{
		size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
		reserve(newcapacity);
	}
	*_finish = val;
	_finish++;
}

4.2 pop_back

无需修改内容,之间--finish即可,有效元素减1

cpp 复制代码
//void pop_back()
//{
//	if (_start == _finish)
//		return;
//	else
//		_finish--;
//}

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

4.3 insert

要在pos位置插入数据,那就得把pos位置空出来,那么pos之后的数据要统一往后移动1位;

但是要分情况:

如果pos>it,直接尾插即可;

如果pos<=it,就需要把*(it+1)=*(it)

cpp 复制代码
iterator insert(iterator pos, const T& val)
{
	assert(pos <= _finish);
	assert(pos >= _start);
	if (_finish == _end_of_storage)
	{
		//满了,扩容
		reserve(capacity() == 0 ? 4 : 2 * capacity());
	}
	T* it = _finish - 1;
	if (it >= pos)
	{
		*(it + 1) = *(it);
		it--;
	}
	*pos = val;
	_finish++;
	return _start;
}

但是这里存在一个问题:就是迭代器失效的问题

当我正常执行5次push_back后,程序 是正常的,但是当我注释掉一行push_back的时候,此时的结果并不是我想要的,这是为什么?

执行5次是正常的,但执行4次却有问题,那么可以推断问题出在扩容上面,于是调试代码发现,_pos和_start在执行完扩容之后,地址不一样了,但是我要在pos位置插入数据,而新空间的地址和pos不一样,这不就是非法访问了吗?

画图来分析一下:

我们知道,当扩容的时候,会释放就空间,并更新_start,_finish,_end_of_storage,但是你并没有更新pos的位置,也就是说pos还愣在那里嘞,此时再对pos位置进行写入,不就是非法访问了,所以这里的主要问题就是更新pos。

所以再扩容之前我们要记录pos的相对位置,再在扩容后的新空间里根据相对位置更新pos!正确的代码如下:

cpp 复制代码
iterator insert(iterator pos, const T& val)
{
	assert(pos <= _finish);
	assert(pos >= _start);
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;//提前记录pos的相对位置
		//满了,扩容
		reserve(capacity() == 0 ? 4 : 2 * capacity());
		pos = _start + len;
	}
	T* it = _finish - 1;
	if (it >= pos)
	{
		*(it + 1) = *(it);
		it--;
	}
	*pos = val;
	_finish++;
	return _start;
}

4.4 erase

首先需要先判断_start==_finish?如果相等无法再删。

相对insert整体把pos位置的数据往后挪动1位,那么erase就时把pos位置的数据往前挪动1位。

cpp 复制代码
iterator erase(iterator pos)
{
	if (_start == _finish)
		return nullptr;
	T* it = pos + 1;
	while (it < _finish)
	{
		//*(it) = *(it + 1);//容易越界访问
		*(it - 1) = *(it);
		it++;
	}
}

**注意:**细节1:it从pos位置开始,往前挪容易越界访问!!!

细节2:vs编译器认为erase之后迭代器是失效的

这里举个典型的例子:

就比如第一种情况:迭代器失效(右侧是修改的代码)

注意:修改后的代码LInux下的g++是能跑通的,但是vs不可以,因为vs认为erase之后失效了,不能访问,失效的迭代器会被vs标记,一旦访问就会报错。

5.迭代器相关的函数

5.1 begin

typedef是为便于封装和根据标准来的,begin函数其实就是封装函数。

cpp 复制代码
typedef T* iterator; //方便迭代器的使用
typedef const T* const_iterator;

iterator begin()
{
	return _start;
}

const_iterator begin() const
{
	return _start;
}

5.2 end

cpp 复制代码
iterator end()
{
	return _finish;
}

const_iterator end() const
{
	return _finish;
}

6. 容器访问相关函数

6.1 operator[ ]

cpp 复制代码
T& operator[](size_t i)
{
	assert(i < size());
	return _start[i];
}

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

注:C++中的stl大多数容器支持下标+[ ]访问,少数不支持!!

相关推荐
爱编码的傅同学2 小时前
【常见锁的概念】死锁的产生与避免
java·开发语言
Tansmjs2 小时前
实时数据可视化库
开发语言·c++·算法
WBluuue2 小时前
Codeforces 1075 Div2(ABC1C2D1D2)
c++·算法
添砖java‘’2 小时前
线程的互斥与同步
linux·c++·操作系统·线程·信息与通信
我什么都学不会2 小时前
Python练习作业3
开发语言·python
2401_838472513 小时前
C++模拟器开发实践
开发语言·c++·算法
初九之潜龙勿用3 小时前
C# 操作Word模拟解析HTML标记之背景色
开发语言·c#·word·.net·office
3108748763 小时前
0005.C/C++学习笔记5
c语言·c++·学习
froginwe113 小时前
MySQL UNION 操作详解
开发语言