当前位置: 首页 > article >正文

【C++】- STL之vector模拟实现

1.vector的介绍

  1. vector是表示可变大小数组的序列容器。
  2. vector采用的连续存储空间来存储元素。意味着也可以采用下标对vector的元素进行访问,和数组一样高效。但是它的大小是可以动态改变的,而且它的大小会被容器自动处理。
  3. vector使用动态分配数组来存储它的元素,当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
  4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比时间需要的存储更大,不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
  5. vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
  6. 与其他动态序列容器相比(deque、list and forward_list),vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其他不在末尾的删除和插入操作,效率更低。比起list和forward_list统一的迭代器和引用更好。

2.vector的基本结构

vector是一个顺序存储的容器,也可以说是一个数组,那么对于它的结构,我们可以用三个迭代器来组成,如下图:

在vector当中,其实迭代器就是一个指针。所以我们的vector类的基本成员变量可以这样设计。

template<class T>
class vector
{
public:
	typedef T* iterator;
	typedef const T* const_iterator;
private:
	iterator _start;
	iterator _finish;
	iterator _endofstorage;
};

3.vector模拟实现

3.1 构造函数

构造函数最主要的有两个:无参构造函数和拷贝构造函数。

3.1.1无参构造函数

无参构造函数只需要将全部迭代器赋值空。 

//无参构造函数:直接全部给nullptr
vector()
	:_start(nullptr)
	,_finish(nullptr)
	,_endofstorage(nullptr)
{}

3.1.2 带参数的构造函数(包括size_t和int类型)

对于带参数的构造函数,当参数类型为size_t时,会先为vector分配指定数量的空间,并为每个元素进行初始化。

vector(size_t n, const T& val = T())
{
	_start = new T[n];
	_finish = _start;
	_end_of_storage = _start + n;
	for (size_t i = 0; i < n; i++)
	{
		*_finish++ = val;
	}
}

3.1.2区间构造函数

区间构造函数接收两个迭代器first和last,由于不同类型的容器迭代器类型可能不同,因此设计成函数模板,将区间内的内容尾插到vector,但是调用push_back接口通过_finish == _endofstorage判断是否满,需要初始化

template<class InoutIterator>
vector(InoutIterator first, InoutIterator last)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

3.1.3 拷贝构造函数

拷贝构造的模拟实现可以通过一个个尾插的方式实现:

vector<int> v2(v1);

即将v1的数据从头到尾遍历一遍,遍历的时候顺便将v1的数据尾插到v2。

 

//拷贝构造函数
//vector<int> v1;
//vector<int> v2(v1);
vecotr(const vector<T>& v)
	:_start(nullptr)
	,_finish(nullptr)
	,_endofstorage(nullptr)
{
	const_iterator it = v.begin();//定义一个迭代器指向v1的头
	while (it != v.end())
	{
		push_back(*it); //将v1的数据一个个尾插到v2中,这里尾插在后面实现
		it++;
	}
}

注意:不要使用memcpy函数拷贝数据,如果数据是内置类型或浅拷贝的自定义类型,使用memcoy是没什么问题的,但如果数据是需要深拷贝的自定义类型(string),就会出现问题,拷贝的数据和源数据指向同一块空间,因此,我们使用for循环依次赋值,调用string的赋值运算符重载完成深拷贝,push_back在实现的时候会调用深拷贝类型的赋值运算符重载。

 3.2 析构函数

将空间释放掉,_start,_finish,_endofstorage置为空指针

//析构函数
~vector()
{
    delete[] _start;
    _start = _finish = _endofstrorage = nullptr;
}

3.3赋值运算符重载

3.3.1传统写法

vector<T>& operator=(const vector<T>& v)
{
	//防止自己给自己赋值
	if (this != &v)
	{
		//1.开辟空间
		T* tmp = new T[v.capacity()];
		//2.将数据拷贝到新空间当中
		for (int i = 0; i < v.size(); i++)
		{
			tmp[i] = v._start[i];
		}
		//3.释放原有空间
		delete[] _start;
		//4.指向新空间
		_start = tmp;
		_finish = _start + v.size();
		_endofstorage = _start + v.capacity();
	}
	return *this;
}

3.3.2现代写法

//赋值运算符重载现代写法
vector<T>& operator=(vector<T> v)
{
	swap(v);
	return *this;
}
//只需要交换this和v的桑指针即可
void swap(vector<T>& v)
{
	//调用标准库中的swap函数
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofstorage);
	
}

注意:传值传参时,自定义类型会调用拷贝构造函数形成形参,形参是实参的一份临时拷贝,因此我们可以让这个形参跟我们的this交换。

这样我们的this就成功被复制为我们想要的值了,this指向的旧空间在交换后被形参v所指向,出了这个作用域之后,形参v会调用其析构函数释放掉this指向的旧空间

因此,只需要传值传参swap交换就可以完成开辟新空间+拷贝数据+释放原有空间+指向新空间

3.4扩容函数 reserve

当n大于对象当前的capacity时,将capacity扩大到n或大于n

当n小于对象当前的capacity是,就不需要

实现步骤:

1.新开辟一块空间,若容器为空,将_start,_finish指向新开辟空间的首元素地址,_endofstorage指向新开辟空间的最后一个元素下一个位置

2.若容器不为空,将数据拷贝到新空间,释放掉旧空间,更新_start,_finish,_endofstorage的位置

注意:将数据拷贝到新空间,仍然不能用memcpy函数,因为对于需要深拷贝的自定义类型,使用memcpy函数以后,新开辟空间里的元素原空间里的元素所指向的内存空间是一样的,当旧空间被释放时,会调用自定义类型的析构函数,从而使得新开辟空间里的元素指向的内存空间也被释放 

void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];//新开辟一块空间
		//容器不为空
		if (_start)
		{
			size_t sizec = size();// 计算原来容器size个数
			for (size_t i = 0; i < size(); i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;//释放旧空间
			_start = tmp; //更新_start
			_finish = _start + sizec;// 更新_finish
			_endofstorage = _start + n; // 更新_endofstorage
		}
		//容器为空,更新_start,_finish,_endofstorage的位置
		else
		{
			_start = _finish = tmp;
			_endofstorage = _start + n;
		}
	}
}

 3.5改变数组长度函数 resize

当n<size时,直接将_finish = _start+n(将有效数据长度缩小)

当size<n<=capacity时,我们将有效数据的长度增加到n,增加出来的有效数据内容是val

当n>capacity时,先调用上面的reserve函数进行增容,再将有效数据的长度增加到你,增加出来的有效数据内容是val

void resize(size_t n, const T& val = T())
{
	//n<size()
	if (n < size())
	{
		_finish = _start + n;
	}
	//n>size()
	else
	{
		//增容
		if (n > capacity())
			reserve(n);
		//填充数据val
		size_t count = n - size();
		while (count--)
		{
			*_finish = val;
			++_finish;
		}
	}
}

 3.6 判空函数 empty

判断size() == 0

bool empty() const
{
    return size() == 0;
}

3.7 尾插函数 push_back

尾插入数据,首先要检查是否已满,已满则进行增容,增容后尾插

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

3.8 尾删函数 pop_back

对于尾删,首先要判断容器是否为空,若为空,则断言报错,不为空,_finish--

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

3.9 插入函数 insert

容量不够,先增容,增容之前先记录下pos - _start的值,否则增容之后,pos还指向原来已经被释放的空间;

将pos位置往后的数据往后挪动一位,在pos位置插入值val;

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

 3.10 删除函数 erase

容器若为空,则做断言处理,若不为空,将pos位置往后的数据向前挪动一位,删除数字之后返回pos位置的迭代器,否则会失去该位置的迭代器,导致失效。

iterator erase(iterator pos)
	{
		assert(pos >= _start);
		assert(pos < _finish);

		iterator it = pos + 1;
		while (it != end())
		{
			*(it - 1) = *it;
			++it;
		}
			
		--_finish;
	}


http://www.kler.cn/news/356513.html

相关文章:

  • wifi、热点密码破解 - python
  • 8-基于双TMS320C6678 + XC7K420T的6U CPCI Express高速数据处理平台
  • oracle数据库---基本查询(单表查询、多表查询、子查询、分页查询、oracle内置函数、行列转换、集合运算)
  • 通过docker镜像安装elasticsearch和kibana
  • 充电桩高压快充发展趋势
  • JPA、Hibernate入门及实战
  • Vmware一步安装win7系统
  • 从零开始搭建:基于在线教育系统源码的线上网校开发详解
  • 软考-软件设计师(10)-专业英语词汇汇总与新技术知识点
  • 基于x86_64汇编语言简单教程1: 环境预备与尝试
  • 25计软新增考研院校!或可捡漏上岸!
  • 「C++」内存管理
  • linux centos7系统ARM架构下安装最新版docker 27.3.1及docker-compose v2.3.4
  • 【ESP32】Arduino开发 | LED PWM控制器+呼吸灯例程
  • 《重置MobaXterm密码并连接Linux虚拟机的完整操作指南》
  • C++类域访问方式(public,protected,private)对象访问 , 通过成员函数访问 ,通过友元函数访问
  • 从新手到高手:Spring AOP的进阶指南
  • 安防综合管理系统EasyCVR视频汇聚平台Linux环境下如何测试UDP端口是否正常开启?
  • 打印机出现线条和残影情况的主要原因和解决办法
  • 项目管理APP推荐_功能对比与用户评价