vector模拟实现

vector模拟实现

  • 构造函数
  • 拷贝构造函数
  • 析构函数
  • 赋值运算符重载
  • 容量大小相关的函数
    • size()
    • capacity()
    • reserve
    • resize
  • 修改容器内容相关函数
    • push_back
    • pop_back
    • insert
    • erase
    • swap
  • 访问容器内容相关函数
    • operator[]
  • 与迭代器相关函数
    • begin()和end()
    • 关于迭代器失效的问题

构造函数

vector中有三个成员变量,_start,_finish,_end_of_sorage,其中_start指向容器最开始的位置,_end_of_sorage指向容器最结尾的位置,_finish指向有效数据结尾的位置。
在这里插入图片描述

方案1: 对于vector的构造函数,我们可以设置一个无参的默认构造函数,将它的成员变量全部初始化为nullptr即可:

//构造自己的命名空间,防止与标准库中的vector产生冲突
namespace gtt
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		vector()
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{}
	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;
	};
}

方案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的数据到容器当中即可。

vector(size_t 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);//尾插数据
	}
}

拷贝构造函数

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

//传统写法
vector(const vector<T>& v)
{
	_start = new T[v.size()];
	//memcpy(_start, v._start, sizeof(T)* v.size())//memcpy默认浅拷贝,会出现问题
	for (size_t i = 0; i < v.size(); i++)
	{
		_start[i] = v._start[i];
	}
	_finish = _start + v.size();
	_end_of_storage = _start + v.size();
}

我们需要注意的是,对于vector< string >,vector< vector< int > >…这样的结构来说,是会进行深拷贝的,而memcpy函数进行的是浅拷贝,就会出现两个指针指向同一片空间的问题,会出现两个问题:1.对同一片空间析构两次;2.一个对象修改会影响另外一个对象。
在这里插入图片描述
在这儿我们的解决办法就是使用循环的方式进行赋值,这样拷贝构造出来的结果就指向了不同的空间,就避免了他们会指向同一块空间的问题:
在这里插入图片描述

现代写法:使用范围for(或是其他遍历方式)对容器v进行遍历,在遍历过程中将容器v中存储的数据一个个尾插过来即可。

//现代写法
vector(const vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	reserve(v.size());
	for (const auto& e : v)//避免又进行深拷贝
	{
		push_back(e);
	}
}

析构函数

进行析构时,先释放容器的存储空间,然后将各个成员变量置为nullptr即可。

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

赋值运算符重载

传统写法:首先判断是否是给自己赋值,若是给自己赋值则无需进行操作。若不是给自己赋值,则先开辟一块和容器v大小相同的空间,然后将容器v当中的数据一个个拷贝过来,最后更新_finish和_endofstorage的值即可。

//传统写法
vector<T>& operator=(const vector<T>& v)
{
	if (this != &v)//判断是否自己给自己赋值
	{
		delete[] _start;//释放掉原空间
		_start = new T[v.size()];//重新开辟空间
		for (size_t i = 0; i < v.size(); i++)
		{
			_start[i] = v[i];//赋值
		}
		_finish = _start + v.size();//有效元素结尾
		_end_of_storage = _start + v.capacity();//整个容器结尾
	}
	return *this;
}

现代写法:右值传参使用传值传参,相当于调用拷贝构造函数,然后将左值与拷贝构造出来容器进行交换,完成赋值,出了函数作用域,拷贝构造出来容器自动销毁

vector<T>& operator=(const vector<T> v)//传值传参
{
	swap(v);//交换数据
	return *this;
}

容量大小相关的函数

size()

返回的就是有效数据的个数,即用_finish - _start即可。

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

capacity()

返回的是容量的大小,即用_end_of_storage - _start即可。

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

reserve

对空间进行扩容,如果n<capacity,就什么也不做,如果n>capacity,就进行扩容,新开辟一个空间,将原空间数据拷贝过来,在释放掉原空间,最后让_start,_finish,_end_of_sorage指向新空间即可。
我们需要注意的是,对于reserve函数,我们依然不可以使用memcpy函数,因为它也会存在深浅拷贝的问题,我们在调用delete函数时,就会对同一片空间释放两次

void reserve(size_t n)
{
	if (n > capacity())//判断是否需要进行扩容
	{
		size_t sz = size();
		T* tmp = new T[n];//开辟新空间
		if (_start)
		{
			//memcpy(tmp, _start, sizeof(T) * sz);//不能使用,存在深浅拷贝问题
			for (size_t i = 0; i < sz; i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;//释放掉原空间
		}

		_start = tmp;//指向新空间
		_finish = _start + sz;//指向新空间
		_end_of_storage = _start + n;//指向新空间
	}
}

resize

1.如果扩容空间大于当前的size,调用reverse函数进行扩容,如果没有给定val值,则调用默认构造函数初始化的值。
2.如果扩容空间小于当前的size,则将size缩小到n;
我们需要注意的是,只有当n>capacity时才会改变容量的大小,其他情况下capacity并不会发生改变。

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

修改容器内容相关函数

push_back

尾插数据,如果满了,就需要进行扩容,没满直接在_finish位置插入数据即可。

//尾插数据
void push_back(const T& x)
{
	if (_finish == _end_of_storage)//判断是否需要进行扩容
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}
	*_finish = x;//插入数据
	_finish++;//改变位置
}

pop_back

尾删数据,删完就不删了,实际就是挪动_finish指向的位置;

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

insert

在任意位置插入数据,将插入位置后面的数据统一向后挪动,需要判断位置的合法性,插入以后需要进行扩容。但是我们需要注意的是,扩容以后使用的就是一片新空间了,旧空间已经被释放掉了,就会出现pos位置找不到的问题,所以我们需要对pos位置做出相应的标记。

void insert(iterator pos, const T& x)
{
	assert(pos <= _finish && pos >= _start);//判断pos合法性
	if (_finish == _end_of_storage)//检查是否需要扩容
	{
		size_t len = pos - _start;//标记pos位置
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;//pos指向新空间
	}
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		end--;
	}
	*pos = x;//插入数据
	_finish++;
}

erase

删除任意位置数据,就需要将删除位置后面的数据统一向前覆盖,stl 规定erase返回删除位置下一个位置迭代器

iterator erase(iterator pos)
{
	assert(pos <= _finish && pos >= _start);//判断pos合法性
	iterator begin = pos + 1;

	while (begin < _finish)
	{
		*(begin - 1) = *begin;//覆盖数据
		begin++;
	}
	_finish--;//改变尾的位置

	return pos;
}

swap

用于交换两个容器里的数据,我们可以直接调用库里面的swap函数交换即可。

void swap(vector<T>& v)
{
	::swap(_satrt, v._start);
	::swap(_finish, v._finish);
	::swap(_end_of_storage, v._end_of_storage);
}

访问容器内容相关函数

operator[]

支持以下标方式访问数据,直接返回相应位置的数据即可;

T& operator[](size_t pos)
{
	assert(pos < size());//判断pos的合法性
	return _start[pos];//返回pos位置数据
}

与迭代器相关函数

begin()和end()

begin()就是返回首地址,end()就是返回有效数据的下一个的地址

在这里插入图片描述

//可读可改
iterator begin()
{
	return _start;
}
iterator end()
{
	return _finish;
}

//可读不可改
const iterator begin() const
{
	return _start;
}
const iterator end() const
{
	return _finish;
}

我们就可以使用迭代器与范围for遍历我们的vector:

vector<int>::iterator it = v.begin();
while (it != v.end())
{
	cout << *it << " ";
	it++;
}
cout << endl;

for (auto e : v)
{
	cout << e << " ";
}
cout << endl;

关于迭代器失效的问题

关于insert函数,会存在两个问题:

  • 我们在插入数据的过程在,如果空间足够,我们就直接插入即可,如果空间不够,就需要调用reserve函数进行扩容,而在扩容过程中就会开辟新空间,将旧空间给释放掉,此时pos是指向旧空间的,释放掉旧空间以后pos就成了野指针,程序直接就崩了,结局办法就是insert函数实现过程中将pos位置标记,即使扩容以后以后,pos依然指向的还是原来的值。
    在这里插入图片描述
  • 这儿还会存在一个问题,假设我们需要在所有偶数前面插入一个该偶数的2倍,就像下面这段程序一样,我们就会发现,程序也崩了,因为我们在pos位置插入数据以后,pos本身位置并没有发生改变,但是原本他指向的值已经向后挪动了,插入完成以后对pos进行++,依然指向的是原本的值,就会无限在该位置前面插入值,
    在这里插入图片描述
    我们的解决方式就是在插入以后接受插入位置的下一个位置迭代器,在直接将pos位置向后移动两位,然后在进行判断,问题就解决了。
auto it = v.begin();
while (it != v.end())
{
	if (*it % 2 == 0)
	{
		it = v.insert(it, *it * 2);//结收插入位置下一个位置迭代器
		it++;
		it++;
		//it先向后移动两位
	}
	//判断奇偶
	else
	{
		it++;
	}
}

对于erase函数,我们再删除完成以后,后面所有的值都会向前挪动一位,比如我们需要删除所有偶数,就会出现不同的情况:
在这里插入图片描述
针对上述情况:我们就需要接收erase删除位置下一个位置的迭代器,然后进行判断。

auto it = v.begin();
while (it != v.end())
{
	if (*it % 2 == 0)
	{
		it = v.erase(it);//接收删除位置下一个位置的迭代器
	}
	else
	{
		it++;
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/63234.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【JVM】垃圾回收 ——自问自答2

Q: System.gc() 的理解 System.gc()底层调用的是 Runtime.getRuntime.gc(),会现实出发FullGC。 但是&#xff0c;它的调用附带一个免责声明&#xff0c;无法保证对垃圾收集器的调用。 Q&#xff1a; 内存溢出和内存泄漏&#xff1f; 内存溢出&#xff1a; 简而言之&#xf…

删除这4个文件夹,流畅使用手机无忧

在现代社会中&#xff0c;手机已经成为我们生活中不可或缺的一部分。然而&#xff0c;随着使用时间的增长&#xff0c;我们可能会遇到手机卡顿和内存不足的问题&#xff0c;让我们感到十分困扰。手机卡顿不仅影响使用体验&#xff0c;还可能导致应用程序运行缓慢&#xff0c;甚…

3分钟白话RocketMQ系列—— 核心概念

白话3分钟&#xff0c;快速了解RocketMQ基础&#xff0c;包括适用场景&#xff0c;以及基本概念。 看完如果不了解&#xff0c;欢迎来打我。 关键字摘要 低延迟、高可用、高可靠、高并发 的消息中间件适合在线业务分为producer、consumer、nameserver、broker等角色另外还有主…

第3章 语言基础

引言 任何语言的核心所描述的都是这门语言在最基本的层面上如何工作&#xff0c;涉及语法、操作符、数据类型以及内置功能&#xff0c;在此基础之上才可以构建复杂的解决方案 本章接下来的内容主要基于ECMAScript第6版。ES6 语法 js的语法借鉴了c/c&#xff0c;java。js是相对…

快速消除视频的原声的技巧分享

网络上下载的视频都会有视频原声或者背景音乐&#xff0c;如果不喜欢并且想更换新的BGM要怎么操作呢&#xff1f;今天小编就来教你如何快速给多个视频更换新的BGM&#xff0c;很简单&#xff0c;只需要将原视频的原声快速消音同时添加新的背景音频就行&#xff0c;一起来看看详…

c语言函数作为形参的注意事项

1、c语言数组作为形参会退化成数组指针 #include "stdio.h" #include <stdlib.h>//数组作为函数的形参会退化成指针 void print_arr(int a[5]);// int *b int main() {int arr[5] { 1,2,3,4,5 };print_arr(arr);return 0; }void print_arr(int a[5]) {printf…

UVA1347 旅行 Tour (样例解释 + 思路心得 + 代码)

目录 题目大意 样例解释 第一组样例解释 第二组样例解释 有注释的代码 没有注释的代码 题目大意 样例解释 &#xff08;多组样例&#xff09; 第一组样例解释 第二组样例解释 &#xff08;没有注释的代码实际上很短 在后面&#xff09; 有注释的代码 思路和心得都在代码里, 快…

【Linux】计算机网络的背景和协议分层

文章目录 网络发展协议何为协议网络协议协议分层OSI七层模型TCP/IP五层模型&#xff08;四层&#xff09; 基本通信流程mac地址和ip地址网络通信本质 网络发展 从一开始计算机作为一台台单机使用&#xff0c;到现在网络飞速发展&#xff0c;从局域网Lan建立起局域网&#xff0…

git bash 安装sdkadmin

1.下载相关安装包,复制到git 安装目录 D:\software\Git\mingw64\bin 2. 运行 curl -s "https://get.sdkman.io" | bash

Service not registered 异常导致手机重启分析

和你一起终身学习&#xff0c;这里是程序员Android 经典好文推荐&#xff0c;通过阅读本文&#xff0c;您将收获以下知识点: 一、Service not registered 异常导致手机重启二、Service not registered 解决方案 一、Service not registered 异常导致手机重启 1.重启 的部分Log如…

某大型医院门户网站性能分析案例

故障现象 门户网站近期出现少量的访问体验慢现象&#xff0c;主要是由于服务器响应时间慢。出现慢页面的页面簇为&#xff1a;http://www.xxx.ac.cn/。 分析过程 下面将分析异常原因:页面的URL信息&#xff1f;页面慢的原因&#xff1f; 性能问题分析&#xff0c;定位到慢访…

不可错过的家装服务预约小程序商城开发指南

在当今社会&#xff0c;家装行业发展迅速&#xff0c;越来越多的人开始寻求专业的家装预约和咨询服务。对于不懂技术的新手来说&#xff0c;创建一个自己的家装预约咨询平台可能听起来很困难&#xff0c;但实际上通过一些第三方制作平台和工具&#xff0c;这个过程可以变得简单…

ES6 数组的用法

1. forEach() 用来循环遍历的 for 数组名.forEach(function (item,index,arr) {})item:数组每一项 , index : 数组索引 , arr:原数组作用: 用来遍历数组 let arr [1, 2, 3, 4]; console.log(arr); let arr1 arr.forEach((item, index, arr) > {console.log(item, index…

客服型电话呼叫中心系统,助力企业提升客户服务质量

客服型电话呼叫中心系统是企业客户服务的重要工具之一&#xff0c;它通过电话和网络等方式&#xff0c;为客户提供快速、便捷、高效的服务。客服型电话呼叫中心系统具备自动接听来电、自动路由、管理知识库、录音和监控、生成报表分析等多种功能&#xff0c;有利于企业提高客户…

【前端 | CSS布局】 网格布局(grid)

概述 网格布局&#xff08;Grid&#xff09;是最强大的 CSS 布局方案。 它将网页划分成一个个网格&#xff0c;可以任意组合不同的网格&#xff0c;做出各种各样的布局。以前&#xff0c;只能通过复杂的 CSS 框架达到的效果&#xff0c;现在浏览器内置了。 上图这样的布局&am…

VS+QT+VTK treeView树型结构模型加载隐藏实例

程序示例精选 VSQTVTK treeView树型结构模型加载隐藏实例 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<VSQTVTK treeView树型结构模型加载隐藏实例>>编写代码&#xff0c;代码…

uniapp h5支付宝支付后端返回Form表单,前端如何处理

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言1.调取接口拿到后端返回的form表单 前言 uniapp h5 支付宝支付&#xff0c;后端返回一串form表单&#xff0c;前端如何拿到支付串并且调用支付 1.调取接口拿到…

内容动态展示抽屉组件

知识点 mousemove与mouseenter的区别在于mousemove会触发事件冒泡&#xff0c;mouseenter不会&#xff0c;mouseleave同理。 mousemove会触发事件冒泡&#xff0c;因此鼠标在范围区域内移动时会一直触发。 mouseenter只触发一次&#xff0c;鼠标移入后触发&#xff0c;鼠标移…

Linux知识点 -- 进程间通信(二)

Linux知识点 – 进程间通信&#xff08;二&#xff09; 文章目录 Linux知识点 -- 进程间通信&#xff08;二&#xff09;一、System V共享内存1.原理2.申请共享内存3.System V共享内存的使用4.为共享内存添加访问控制 二、信号量&#xff08;概念理解&#xff09;1.概念2.信号量…

JVM 调优

点击下方关注我&#xff0c;然后右上角点击...“设为星标”&#xff0c;就能第一时间收到更新推送啦~~~ JVM调优是一项重要的任务&#xff0c;可以提高Java应用程序的性能和稳定性。掌握JVM调优需要深入了解JVM的工作原理、参数和配置选项&#xff0c;以及历史JVM参数的调整和优…