【STL】vector的模拟实现

目录

前言 

vector概述

vector的数据结构

vector迭代器的运用 

vector的构造和析构

vector的拷贝构造与赋值 

拷贝构造 

传统写法

现代写法

 赋值重载

vector的扩容 

reserve() 

 resize()

vector的元素操作

push_back() 

pop_back() 

insert()

erase() 

迭代器失效问题 

insert中迭代器失效问题

 erase中迭代器失效问题

完整代码链接


前言 

关于本篇可以先去看看上篇的string的模拟实现,更好的理解这里的内容。

关于vector容器的详细用法可以参考这个网站——vector

如果想更深的了解vector的底层实现的话可以看看侯捷老师写的《STL源码剖析》 

本文内容也参考了《STL源码剖析》中的内容

vector概述

vector是表示可变大小数组的序列式容器,就像数组一样,vector也采用了连续的空间来存储元素。这也就意味着可以采用下标对元素进行访问。vector是动态空间,随着元素的插入,它的内部机制会自动扩充空间来存储新数据,而且扩充的空间比实际存储的空间会更大,这是出于一种位于绸缪的考虑。这种有效的动态增长方式与其它动态序列式容器相比,vector在访问元素时更加高效。

vector的数据结构

vector的数据结构实现起来也非常简单:连续的线性空间。库里面的vector本身就是支持任何元素类型都可以用迭代器的方式进行访问,所以我们这里的实现的迭代器也要支持同样的操作——使用类模板,用两个迭代器start和finish分别指向分配来的连续空间中目前已经使用的范围,还有一个迭代器end_of_storage指向目前可用空间的尾。

vector迭代器的运用 

运用_start,_finish,_end_of_storage三个迭代器,便可轻易的提供首尾标示、大小、容量、 [](下标访问)、最前端元素和最后端元素

    typedef T* iterator;
	typedef const T* const_iterator;


	iterator begin()const
	{
		return _start;
	}

	iterator end()const
	{
		return _finish;
	}

	const_iterator cbegin()
	{
		return _start;
	}

	const_iterator cend()
	{
		return _finish;
	}

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

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

    T& operator[](size_t pos)
	{
		assert(pos < size());
		return _start[pos];//=*(_start + pos)
	}

	const T& operator[](size_t pos)const 
	{
		assert(pos < size());
		return _start[pos];//=*(_start + pos)
	}

    T& front()
	{
		assert(size() > 0);
		return *_start;
	}

	T& back()
	{
		assert(size() > 0);
		return *(_finish - 1);
	}

vector的构造和析构

 vector的构造和析构是比较常规的操作了,也就是把上面的数据结构进行初始化和清理 

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

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

迭代器区间构造

	template <class InputIterator>
	MyVector(InputIterator first, InputIterator last)
		:_start(nullptr)
		, _finish(nullptr)
		, _end_of_storage(nullptr)
	{
		while (first != last)
		{
			push_back(*first);
			first++;
		}
	}

 这里使用的函数模板和上面的类模板是可以在一起使用的。

 指定大小初始化构造

    MyVector(size_t n, const T& value = T())
		:_start(nullptr)
		,_finish(nullptr)
		,_end_of_storage(nullptr)
	{
		reserve(n);
		while(n--)
		{
			push_back(value);
		}
	}

	MyVector(int n, const T& value = T())
		:_start(nullptr)
		, _finish(nullptr)
		, _end_of_storage(nullptr)
	{
		reserve(n);
		for (size_t i = 0; i < n; i++)
		{
			push_back(value);
		}
	}

 理论上提供了MyVector(size_t n, const T& value = T())之后,MyVector(int n, const T& value = T())接口可以不需要提供了,但是对于MyVector<int> v(10,5)这种,10和5都是int类型,编译器在编译时,会找和它最匹配的构造函数,n是size_t类型,T被示例化成了int类型,所以会选择MyVector(InputIterator first, InputIterator last),而不会选择MyVector(size_t n, const T& value = T()),因为编译器认为区间构造两个参数类型是一样的,所以会将InputIterator实例化成int,但是10和5根本不是一个区间,编译器编译时就会报错。因此需要增加这个接口。包括库里面也是这样实现的。

vector的拷贝构造与赋值 

拷贝构造和赋值如果我们不写编译器就会默认生成,而默认生成的是浅拷贝,在析构的时候就会出问题。

拷贝构造 

传统写法

    MyVector(const MyVector<T>& v)
	{
		_start = new T[v.size()];
		memcpy(_start, v._start, sizeof(T) * v.size());
		_finish = _start + v.size();
		_end_of_storage = _start + v.size();
	}

这种写法是大多数人都能想出来的,但是这里有个非常致命的问题就是使用了memcpy。

 

当你这样写的话,程序就会崩溃。 

memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中,如果拷贝的是内置类型的元素时,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型中涉及到资源管理时就会出错,因为memcpy实际上是一个浅拷贝。

 所以正确的写法是下面这样的

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

但是有些人还会这样写,也是OK的

    MyVector(const MyVector<T>& v)
		:_start(nullptr)
		,_finish(nullptr)
		,_end_of_storage(nullptr)
	{
		reserve(v.size());
		for (const auto& d : v)
		{
			push_back(d);
		}
	}

现代写法

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

	//现代写法
	MyVector(const MyVector<T>& v)
		:_start(nullptr)
		,_finish(nullptr)
		,_end_of_storage(nullptr)
	{
		MyVector tmp(v.begin(), v.end());
		swap(tmp);
	}

在现代写法中用到了一个迭代器区间的构造函数。而这里的tmp扮演的就是一个打工人的角色,勤勤恳恳的给老板(v)打工。

 赋值重载

 这里就直接上现代写法了,毕竟现代写法更简单

    MyVector<T>& operator=(MyVector<T> v)
	{
		swap(v);
		return *this;
	}

vector的扩容 

reserve() 

reserve实现起来也很简单,首先是判断空间够不够,如果不够就需要扩容,在扩容时需要开辟新空间,并把原来空间中的元素拷贝到新空间去,之后更新迭代器,所以我们很容易就会写出下面的代码。

	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);
				delete[] _start;
			}

			_start = tmp;
			_finish = _start + sz;
			_end_of_storage = _start + n;
		}
	}

这样就和上面拷贝构造出现的问题一样了,对于自定义类型我们要完成的是一个深拷贝而不是浅拷贝。

正确写法 

    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()

 对于resize我们要考虑三种情况

  1. 当n > capacity()时,我们需要扩容+初始化
  2. 当n > size()并且n < capacity()时,我们只需要初始化
  3. 当n < size()时,我们需要删除多余的数据

    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;
		}
	}

vector的元素操作

push_back() 

先判断空间够不够,如果不够就需要扩容,然后再将元素插入进去 

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

pop_back() 

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

insert()

在实现insert时,要注意扩容带来的迭代器失效问题,我们要保存一下pos位置,以免扩容时,pos迭代器失效。

之后往后挪动数据(从后往前挪动数据)将pos位置空出来,以便插入

	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;
	}

erase() 

和insert类似我们也需要挪动数据将pos位置的数据覆盖掉即可

 

	iterator erase(iterator pos)
	{
		assert(pos >= _start && pos < _finish);
		iterator begin = pos + 1;
		while (begin < _finish)
		{
			*(begin - 1) = *begin;
			begin++;
		}

		_finish--;
		return pos;
	}

迭代器失效问题 

insert中迭代器失效问题

在insert插入元素时,如果空间不够会发生扩容,扩完容后原来的空间就会释放掉,但是你的addr还是指向的是原来的空间,所以当你访问addr位置时,就会发生野指针问题。

所以当你用了一次addr最好就不要在用了

 erase中迭代器失效问题

 删除数据又是如何造成迭代器失效的呢?我们可以看一看下面的代码

下面这种情况是没有问题,正常运行的 

int main()
{
	MyVector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	//删除所有的偶数
	auto it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		it++;
	}
	for (auto v : v1)
	{
		cout << v << " ";
	}
	return 0;
}

 但是我们再增加一个数,它的结果就会出现问题了

int main()
{
	MyVector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	//删除所有的偶数
	auto it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		it++;
	}
	for (auto v : v1)
	{
		cout << v << " ";
	}
	return 0;
}

下面这个就会运行崩溃

int main()
{
	MyVector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	v1.push_back(6);

	//删除所有的偶数
	auto it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		it++;
	}
	for (auto v : v1)
	{
		cout << v << " ";
	}
	return 0;
}

这都是erase迭代器失效造成的,让我们来分析分析

 对于第二个代码

 这就是第二个结果导致的原因

对于第三个代码

在erase的实现中,我们是使用了在pos位置从前往后进行数据覆盖,并且erase返回的是当前删除数据的下一个位置,但是我们完成了一个数据覆盖,所以返回的还是当前删除数据的位置。

所以上面代码正确的写法是这样的

    auto it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		else
		{
			it++;
		}
	}

完整代码链接

 vector的模拟实现


 今天的分享就到这里了,如果内容有错的话还望指出,谢谢!!!

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

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

相关文章

[linux初阶][vim-gcc-gdb] TwoCharter: gcc编译器

目录 一.Linux中gcc编译器的下载与安装 二.使用gcc编译器来翻译 C语言程序 ①.编写C语言代码 ②翻译C语言代码 a.预处理 b.编译 c.汇编 d.链接 ③.执行Main 二进制可执行程序(.exe文件) 三.总结 一.Linux中gcc编译器的下载与安装 使用yum命令(相当于手机上的应用…

HTTPS RSA 握手解析(计算机网络)

传统的 TLS 握手基本都是使用 RSA 算法来实现密钥交换的&#xff0c;在将 TLS 证书部署服务端时&#xff0c;证书文件其实就是服务端的公钥&#xff0c;会在 TLS 握手阶段传递给客户端&#xff0c;而服务端的私钥则一直留在服务端。 在 RSA 密钥协商算法中&#xff0c;客户端会…

算法学习——LeetCode力扣动态规划篇5(198. 打家劫舍、213. 打家劫舍 II、337. 打家劫舍 III )

算法学习——LeetCode力扣动态规划篇5 198. 打家劫舍 198. 打家劫舍 - 力扣&#xff08;LeetCode&#xff09; 描述 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统…

Python|OpenCV-实现检测人脸以及性别检测(12)

前言 本文是该专栏的第13篇,后面将持续分享OpenCV计算机视觉的干货知识,记得关注。 性别检测是计算机视觉领域里面的一个重要学习领域,简单的来说,它可以实现自动识别一张图片中的人物性别。为此在本文中,笔者将结合OpenCV和Tensorflow来实现对一张图进行“图片中的人物人…

探索c++:string常用接口 迷雾

个人主页&#xff1a;日刷百题 系列专栏&#xff1a;〖C/C小游戏〗〖Linux〗〖数据结构〗 〖C语言〗 &#x1f30e;欢迎各位→点赞&#x1f44d;收藏⭐️留言&#x1f4dd; ​ ​ 一、string类 这里我们对string类进行一个简单的总结&#xff1a; string是表示字符串的字…

【更新】在湘源7、8中使用2023年11月国空用地用海分类

之前为了做控规&#xff0c;从湘源8中扒了一套国空用地用海的绘图参数给湘源7使用。 【预告】在湘源控规7中使用 国空用地用海分类标准 但是部里在2023年11月又发布了一套新的用地用海分类。 本想去湘源8里面再扒一下&#xff0c;结果发现湘源8自己还没有更新呢&#xff0c;…

使用STM32 MCU模拟实现PPS+TOD授时信号

简介 PPSTOD是授时信号的一种&#xff0c;用来传递准确的时间信息。 PPS&#xff0c;Pulse Per Second&#xff0c;是每秒一次的脉冲信号&#xff0c;其上升沿表示整秒的时刻。TOD&#xff0c;Time of Day&#xff0c;是时间信息。是跟随在每个PPS信号后的由串口发出的一句报…

Servlet Response的常用方法 缓存和乱码处理

前言 Servlet Response相关的信息&#xff0c;在service方法中使用的是HttpServletResponse&#xff0c;它继承自ServletResponse&#xff0c;扩展了Http协议相关的内容&#xff0c;下面简单记录一下它的基本用法。 一、response组成内容 以下是一个常见response响应的内容&…

红黑树介绍及插入操作的实现

&#x1f389;个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名乐于分享在学习道路上收获的大二在校生 &#x1f648;个人主页&#x1f389;&#xff1a;GOTXX &#x1f43c;个人WeChat&#xff1a;ILXOXVJE &#x1f43c;本文由GOTXX原创&#xff0c;首发CSDN&…

node.js的错误处理

当我打开一个不存在的文件时&#xff0c;错误如下&#xff1a; 在读取文件里面写入console.log&#xff08;err&#xff09;&#xff0c;在控制台中可以看到我的错误代码类型&#xff1a;文件不存在的错误代码 ENOENT。见更多错误代码---打开node.js官方API文档Error 错误 | N…

高炉项目中DeviceNET到Ethernet的转换奥秘

在工业自动化的世界中&#xff0c;高炉项目中的数据通信至关重要。其中DeviceNET和Ethernet作为两种主流的网络协议&#xff0c;扮演着不可或缺的角色。它们之间的转换不仅仅是技术上的桥梁&#xff0c;更是实现信息高效传递的关键。今天&#xff0c;我们就来揭开从DeviceNET到…

每日面经分享(pytest入门)

1. pytest具有什么功能 a. 自动发现和执行测试用例&#xff1a;pytest可以自动发现项目中的测试文件和测试函数&#xff0c;无需手动编写测试套件或测试运行器。 b. 丰富的断言函数&#xff1a;pytest提供了丰富的断言函数&#xff0c;方便地验证测试结果是否符合预期。断言函…

【STM32 HAL库SPI/QSPI协议学习,基于外部Flash读取】

1、SPI协议 简介 SPI 协议是由摩托罗拉公司提出的通讯协议 (Serial Peripheral Interface)&#xff0c;即串行外围设备接口&#xff0c;是一种高速全双工的通信总线。它被广泛地使用在 ADC、LCD 等设备与 MCU 间&#xff0c;要求通讯速率较高的场合。 通信方式&#xff1a;同…

基于SpringBoot+Vue交通管理在线服务系统的开发(源码+部署说明+演示视频+源码介绍+lw)

您好&#xff0c;我是码农飞哥&#xff08;wei158556&#xff09;&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。&#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精通…

Docker搭建LNMP环境实战(10):大结局!脚本化一次性安装测试、生产环境

实现使用 Docker 在一台服务器上搭建支持 80、443 端口访问的测试、生产双站点系统。 1、生产环境&测试环境的规划和部署 1.1、说明 图1 系统部署示意图 1&#xff09;项目 此处以一个演示项目的形式来进行环境的规划和部署。此项目名称默认定义为&#xff1a;“demo”&a…

网安学习笔记-day11,FTP服务器

FTP服务器 FTP介绍 FTP(File Transfer Protocol)文件传输协议 端口号&#xff1a;TCP 20/21 工作方式&#xff1a; 主动模式被动模式 服务器部署 准备阶段 配置IP windowsXP 192.168.1.21&#xff08;也可DHCP自动获取&#xff09; Windows2003 192.168.1.1 安装万维网…

[SWPUCTF 2021 新生赛]crypto5(小明文攻击)

题目&#xff1a; 直接暴力破解&#xff1a; from Cryptodome.Util.number import * import gmpy2 flag 251667516535309413648396638468065433877208653392633709079856557751521873194647157371165991714772070474300653458826262598807568390941796270326238953302426553…

Mybatis-Plus分页查询时碰到`total`有值但`records`为空

个人原因&#xff1a;Mybatis-Plus分页插件设置了maxLimit单页条数 // 分页插件配置 PaginationInnerInterceptor paginationInnerInterceptor new PaginationInnerInterceptor(DbType.MYSQL); paginationInnerInterceptor.setMaxLimit(200L); // 单页分页条数限制(默认无限…

Mysql重点思考(上)--mysql的索引优化

mysql的索引优化 expalin关键字的用法explain索引优化示例 type列用法执行查询的顺序类型概述 索引概念索引的定义索引的分类主键&唯一区别 唯一索引的创建和查询创建一个唯一索引查询一个唯一索引 场景题合集唯一索引的场景题主键索引的场景题&#xff08;B树&#xff09;…

Python下载bing每日壁纸并实现win11 壁纸自动切换

前言: 爬虫哪家强,当然是python 我是属于啥语言都用,都懂点,不精通,实际工作中能能够顶上就可以。去年写的抓取bing每日的壁纸&#xff0c;保存到本地&#xff0c;并上传到阿里云oss&#xff0c;如果只是本地壁纸切换&#xff0c;存下来就行&#xff0c;一直想做个壁纸站点&…