深入篇【C++】谈vector中的深浅拷贝与迭代器失效问题

深入篇【C++】谈vector中的深浅拷贝与迭代器失效问题

  • Ⅰ.深浅拷贝问题
    • 1.内置类型深拷贝
    • 2.自定义类型深拷贝
  • Ⅱ.迭代器失效问题
    • 1.内部迭代器失效
    • 2.外部迭代器失效

Ⅰ.深浅拷贝问题

1.内置类型深拷贝

浅拷贝是什么意思?就是单纯的值拷贝。
浅拷贝的坏处:

①空间会析构两次。
②一个修改会影响另一个。

在这里插入图片描述
根据上一篇vector的模拟实现中需要用到拷贝的有三个函数,一个是拷贝构造,一个是赋值重载,一个是扩容。都需要用深度拷贝。深度拷贝是什么意思呢?
就是开出一块空间,将原空间的数据拷贝过来。有时候还需要将原空间释放掉。
在这里插入图片描述
我们可以看一下手搓的vector中的扩容,就是采用的深度拷贝。

void reserve(size_t n)
		{
			size_t sz = size();
			if (n > capacity())
			{
				T* temp = new T[n];//首先开空间
				if (_start != nullptr)
				{
					//将数据拷贝到temp去
					memcpy(temp, _start, sizeof(T) * sz);
					//删除原来空间
					delete[] _start;
				}
				//最后将空间赋值给_start
				_start = temp;
				_finish = _start + sz;
				//这里有一个问题,size()的计算是用_finish -start 而这里的start已经改变,而finish还没有改变
				//最后计算finish就变成空了,最终的问题在于start改变了,所有在之前要保留一份size()的数据
				_endstroage = _start + n;
			}
		}

在这里插入图片描述
这里利用memcpy函数将数据按照字节的方式将start指向的数据一个一个拷贝到temp指向的空间里。
这个是没有问题的,有问题的是这种情况:当vector<string> 类型的数据扩容时,会有隐藏的深拷贝。
我们知道上面的数据是vector<int>类型的。即内置类型,内置类型使用memcpy按照字节的方式拷贝是没有问题的。
但是不是内置类型而是自定义类型时,利用memcpy拷贝可以吗?

void Test4()
{
	tao::vector<string> v;
	v.push_back("11111111111111111111111");
	v.push_back("22222222222222222222222");
	v.push_back("33333333333333333333333");
	v.push_back("44444444444444444444444");
	//v.push_back("55555555555555555555555");

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

在这里插入图片描述
当前数据实际大小并没有超过容量,所以没有扩容, 也没有发生拷贝,所以正常,但一旦我们再插入一个数据,就会扩容,这时原来的空间就会被释放,那么能正常打印吗?在这里插入图片描述

很明显运行错误,为什么呢?
在这里插入图片描述

2.自定义类型深拷贝

memcpy是按照字节拷贝的,拷贝完后的数据内容肯定是一样的,所以_str会指向同一块数据。而当拷贝完后原空间就会被释放掉,则temp里的_str就变成野指针了。
vector是深拷贝,但vector空间上存的对象是string类型的数组,使用memcpy会导致string对象的浅拷贝。
【解决方案】
所以我们期望这个对象能进行深拷贝,就比如这个对象是string类型的,我们希望能调用这个自定义类型的深拷贝。而对于那些深拷贝的自定义类型来说,赋值重载必须是深拷贝的,所以我们可以使用这个自定义类型的赋值重载来完成深拷贝。

//扩容------>
void reserve(size_t n)
		{
			size_t sz = size();
			if (n > capacity())
			{
				T* temp = new T[n];//首先开空间
				if (_start != nullptr)
				{
					//将数据拷贝到temp去
					//memcpy(temp, _start, sizeof(T) * sz);
					for (size_t i = 0; i < sz; i++)
					{
						temp[i] = _start[i];
						//比如_start[i] 是string类型的数据,那么这里赋值给temp就会调用赋值运算符重载,而string的赋值运算符重载是深度拷贝的。
					}
					//删除原来空间
					delete[] _start;
				}
				//最后将空间赋值给_start
				_start = temp;
				_finish = _start + sz;
				//这里有一个问题,size()的计算是用_finish -start 而这里的start已经改变,而finish还没有改变
				//最后计算finish就变成空了,最终的问题在于start改变了,所有在之前要保留一份size()的数据
				_endstroage = _start + n;
			}
		}

在这里插入图片描述
这样原空间释放了,并不会影响temp空间里的数据。这就是隐藏的深拷贝,对于vector<T>呢,如果T是内置类型,使用memcpy就可以解决深浅拷贝,但对于T是自定义类型,memcpy就无法完成深拷贝了,需要使用到T类型的赋值重载来深度拷贝。不过这种方法对于内置类型也是可以完成任务的。
所以我们应该将有拷贝的地方都换成上面的写法,而不是用memcpy来完成拷贝。

//拷贝构造------->
	vector(const vector<T>& v)//深拷贝
			: _start(nullptr)
			, _finish(nullptr)
			, _endstroage(nullptr)
		{
			_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();
			_endstroage = _start+v.capacity();
		}

Ⅱ.迭代器失效问题

1.内部迭代器失效

迭代器在遍历访问的时候非常好用,但有的情况下迭代器会发生失效。
什么情况下迭代器会发生失效呢?

①会引起底层空间改变的操作,都有可能引起迭代器失效,比如insert或者push_back插入一个数据时,发生扩容。
②erase删除一个数据时,迭代器发生失效。

什么叫迭代器失效呢?就是不能再访问这个迭代器了,正常使用这个迭代器了。
vector和string都有insert和erase,为什么string没有这个问题呢?因为string中的insert和erase用的不是迭代器而是下标。
在这里插入图片描述
比如insert(iteraort pos,const T&val)在pos位置插入数据时,首先会检查是否需要扩容,发现需要扩容时,会开出2倍空间,将原数据拷贝下来,然后再将原空间释放,最后将temp空间赋给start。
这时就要注意到pos迭代器就已经失效了,为什么?因为pos迭代器原先指向的空间被释放了,现在的pos迭代器就类似一个野指针,危险的很,不能再去使用了。那这时insert插入的pos位置就是一个未知的空间了,肯定会报错的。
【解决方法】
这里pos因为原空间被释放了,变成野指针了,扩容完后的start指向的空间正常,但再去往pos位置去插入数据这就危险了。问题就在于pos位置,我们应该在扩容直接记录一下pos的相对位置,然后扩容后,再更新一下pos的新位置,这样pos迭代器就不会失效了。

iterator insert(iterator pos,const T &val)
		{
			assert(pos >= _start && pos <= _finish);
			//首先考虑扩容----这里有一个问题:迭代器失效
			//当迭代器扩容时,这里的pos迭代器就相当于失效了,因为原来的空间被释放了,pos也就变成野指针了。
			//需要将将pos迭代器恢复,需要更新pos的新位置。
			if (_finish == _endstroage)
			{
------------->  size_t len = pos - _start;
				size_t newcapacity = (capacity() == 0 ? 4 : capacity() * 2);
				reserve(newcapacity);
				pos = _start + len;
			}
			//使用迭代器的好处就是可以避免string那样头插时,挪动数据,下标要小于0的问题,因为迭代器是一个地址,不可以为0的
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				end--;
			}
			*pos = val;
			_finish++;
			//insert 中的扩容迭代器失效,外部迭代器的解决方法是使用返回值,将pos位置返回过去,再用迭代器接收,就可以对pos位置上的内容再访问了
			return pos;
			//指向新插入位置的迭代器
		}

2.外部迭代器失效

v里面已经有了4个数据1,2,3,4这时候再在第三个位置插入一个800。
然后再对这个插入位置上的数据修改一下,给这个数据加上1000。
这里的迭代器失效的原因还是因为原空间被销毁,pos位置变成野指针了。要注意虽然我们已经完善了内部迭代器失效,但因为这里是传值传参,形参的改变不会影响实参,虽然形参的迭代器不会失效,但是实参还是会失效的。
所以这里再次访问这个it指向的空间时,就是非法的了,因为it指向的空间被释放了。

tao::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	tao::vector<int>::iterator it = v.begin()+3;
	v.insert(it, 800);//这里的it--->pos 形参的改变不影响实参
	//这里的it迭代器失效了,访问的不是第三个位置了。
	*it += 1000;
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

在这里插入图片描述
而标准库里的insert也会遇到这样的问题,那么库里的insert是如何解决这个问题的呢?
库里是用返回值的方法来解决的,也就的将插入数据的位置返回回去,用it来接收,那么虽然形参改变无法改变实参,但最后将改变后的形参以返回值的方式再传给实参,实参就有效了。
在这里插入图片描述

tao::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	tao::vector<int>::iterator it = v.begin()+3;
	it=v.insert(it, 800);
	//用返回值的方式将有效的迭代器(指向插入元素的位置)传送回来,那样it就可以使用访问了。
	*it += 1000;
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

在这里插入图片描述

以上就是insert插入操作引起的迭代器失效问题。接下来我将介绍因为erase删除操作而引起的迭代器问题。
erase操作为什么会引起迭代器失效呢?

1.这里的问题不是类似于空间销毁,变成野指针了,而是这个迭代器代表的位置的意义改变了,指向的内容不一样了。删除pos位置上的元素,pos位置之后的元素就会往前挪动覆盖,pos位置的元素就变成了一个新的元素了。在VS下这个行为就认定为迭代器失效了。
2.即erase以后,迭代器就失效,不能再访问,vs会进行强制检查,如果访问会直接报错。

以下面这个代码为例子:删除vector中的所有偶数

tao::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(4);
	v.push_back(4);
	v.push_back(4);
	v.push_back(6);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	tao::vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		if ((*it) % 2 == 0)
		{
			 v.erase(it);//这里erase完,it迭代器就失效了,就无法再访问其内容了,比如连续2个的偶数,第一个被删除后,it位置就变成第二偶数了,但这个位置已经失效了,所以这个偶数就无法正常删除掉了。
	//1  2  2  3  第一个2可以删除,第二个2无法删除掉。		 
		}
		else
		{
			++it;//迭代器已经失效了,不能再对这个迭代器操作了。
		}
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

而正常做法应该是,每次删除完这个位置上的元素后,都要重新赋值更新一下it,库里的做法就是将erase删除位置的下一个元素的位置返回回去,这样it就会更新成被删除元素的的下一个位置。

tao::vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		if ((*it) % 2 == 0)
		{
			 it=v.erase(it);//正常使用:在每次删除完后,对迭代器重新赋值即可。	 
		}
		else
		{
			++it;
		}
	}

Linux和vs下对于erase删除元素后迭代器是否失效是不同的,VS下对于迭代器的失效检测非常严格,非常极端,而g++的编译器对于迭代器检测就不是很严格了。

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

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

相关文章

java项目之班级同学录网站(ssm+mysql+jsp)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的班级同学录网站。技术交流和部署相关看文章末尾&#xff01; 开发环境&#xff1a; 后端&#xff1a; 开发语言&#xff1a;Java 框架&a…

基于STM32的homeassistant(采用FreeRTOS操作系统)【第一、二章优化拓展:Wifi、服务器连接验证以及UASRT串口区分】

第一、二章优化拓展开发环境&#xff1a; 主控STM32F103C8T6WIFI模块ESP01S开发语言C开发编译器 KEIL 组网方式WIFI服务器协议MQTT 硬件连接 STM32ESP01S3.3V3.3V GND GND GPIO2 (USRAT2-TX) RXGPIO3 (USART3-RX)TX 本章要点&#xff1a; 对ESP01S的AT指令的反馈指令进…

Kafka消息监控管理工具Offset Explorer的使用教程

1、kafka监控管理工具 Offset Explorer是一款用于监控和管理Apache Kafka集群中消费者组偏移量的开源工具。它提供了一个简单直观的用户界面&#xff0c;用于查看和管理Kafka消费者组偏移量的详细信息。 Offset Explorer具有以下主要功能和特点&#xff1a; 实时监控&#x…

Java开发中使用sql简化开发

引语&#xff1a; 在Java开发中&#xff0c;我们更希望数据库能直接给我们必要的数据&#xff0c;然后在业务层面直接进行使用&#xff0c;所以写一个简单的sql语句有助于提高Java开发效率&#xff0c;本文由简单到复杂的小白吸收&#xff0c;还请多多指教。 使用MySQL数据库…

微服务系列文章 之 SpringCloud中遇到的一些bug

1、There was a problem with the instance info replicator 错误原因&#xff1a; 该服务尝试将自己作为客服端注册解决办法&#xff1a; 在application.yml配置文件中&#xff0c;设置 # 注册Eureka服务 eureka:client:# Eureka服务注册中心会将自己作为客户端来尝试注册它自…

Unity基础 弹簧关节SpringJoint

弹簧关节 在游戏开发中&#xff0c;物体之间的交互性是非常重要的。为了模拟现实世界中的弹性特性&#xff0c;Unity提供了弹簧关节&#xff08;Spring Joint&#xff09;组件。通过弹簧关节&#xff0c;我们可以轻松实现物体之间的弹性交互效果。本文将详细介绍Unity中的弹簧…

OpenCv之Canny

目录 一、自适应阈值 二、边缘检测Canny 一、自适应阈值 引入前提:在前面的部分我们使用是全局闻值&#xff0c;整幅图像采用同一个数作为闻值。当时这种方法并不适应与所有情况&#xff0c;尤其是当同一幅图像上的不同部分的具有不同亮度时。这种情况下我们需要采用自适应闻…

如何清除视频和照片中水印的几种方式

文章目录 如何清除视频和照片中水印的几种方式一、清除视频中水印的几种方式1、截除水印区域2、模糊水印区域3、使用人工智能技术工具3.1 通过【iMyFone-MarkGo[^1]】消除水印3.2 通过【嗨格式视频转换器[^2]】消除水印3.3 通过【PR 视频编辑器】消除水印3.4 通过 【美图秀秀】…

【运维小知识】(一)——centos系统安装(小白入门级)

目录 1.制作系统U盘 2.安装centos系统 3.系统配置 3.1【语言】配置​编辑 3.2【软件选择】配置 3.3【安装位置】配置 3.4【主机名、root密码、网络】配置 1.制作系统U盘 首先下载软件ventoy&#xff0c;制作系统U盘&#xff0c;买个新U盘。先在笔记本电脑安装ventoy软件&a…

利用数据分析告警机制,实现鸿鹄与飞书双向集成

需求描述 实现鸿鹄与飞书的双向集成&#xff0c;依赖鸿鹄的告警机制&#xff0c;可以发送用户关心的信息到飞书。同时依赖飞书强大的卡片消息功能&#xff0c;在飞书消息里面能够通过链接&#xff08;如下图&#xff09;返回到鸿鹄以方便用户进一步排查和分析问题。 解决方案 1…

旅游卡加盟代理合伙人模式软件开发

旅游卡加盟代理合伙人模式是近年来逐渐兴起的一种旅游产业发展模式&#xff0c;它通过将旅游卡加盟商与代理商紧密结合&#xff0c;实现资源共享、风险共担、合作共赢的目标。而软件开发作为旅游卡加盟代理合伙人模式的重要技术支持&#xff0c;对于该模式的实施和发展起着至关…

【Linux系统】结合有趣的小故事让你学懂生产者消费者模型

目录 由故事引入模型故事背景供货商们的矛盾市民们和供货商之间的矛盾一市民们和供货商之间的矛盾二市民们的矛盾模型总结 生产者消费者模型为什么要使用生产者消费者模型&#xff1f;生产者消费者模型的特点生产者消费者模型优点 基于BlockingQueue的生产者消费者模型C queue模…

行为式验证码(成语点选)(C#版和Java版)

一、先看效果图 二、背景介绍 图形验证码网上有挺多&#xff0c;比如&#xff1a;网易易盾、腾讯防水墙、阿里云验证码等等。参考了一下&#xff0c;自己实现了一个简单的成语点选的模式。 三、实现思路 1.选择若干张图片&#xff08;这里使用的是320x160的尺寸&#xff09;…

【Linux】生产者消费者模型 -- RingQueue

文章目录 1. 信号量1.1 信号量的引入1.2 信号量的概念1.3 信号量函数 2. 二元信号量模拟实现互斥功能3. 基于环形队列的生产消费模型3.1 空间资源和数据资源3.2 生产者和消费者申请和释放资源3.3 必须遵守的两个规则3.4 代码实现3.5 信号量保护环形队列的原理 1. 信号量 1.1 信…

Java 串口通讯 Demo

为什么写这篇文章 之前职业生涯中遇到的都是通过tcp协议与其他设备进行通讯&#xff0c;而这个是通过串口与其他设备进行通讯&#xff0c;意识到这里是承重板的连接&#xff0c;但实际上比如拉力、压力等模拟信号转换成数字信号的设备应该是有相当一大部分是通过这种方式通讯的…

6.溢出的文字省略号显示

6.1单行文本溢出显示省略号 必须满足三个条件 /*1. 先强制一行内显示文本*/ white-space: nowrap; &#xff08; 默认 normal 自动换行&#xff09; /*2. 超出的部分隐藏*/ overflow: hidden; /*3. 文字用省略号替代超出的部分*/ text-overflow: ellipsis;【示例代码】 <…

Redis学习(三)持久化机制、分布式缓存、多级缓存、Redis实战经验

文章目录 分布式缓存Redis持久化RDB持久化AOF持久化 Redis主从Redis数据同步原理全量同步增量同步 Redis哨兵哨兵的作用和原理sentinel&#xff08;哨兵&#xff09;的三个作用是什么&#xff1f;sentinel如何判断一个Redis实例是否健康&#xff1f;master出现故障后&#xff0…

Windows下PyTorch深度学习环境配置(GPU)

一&#xff1a;下载Anaconda &#xff08;路径最好全英文&#xff09; &#xff08;下载好后&#xff0c;可以创建其他虚拟环境&#xff0c;因为是自己学习&#xff0c;所以先不放步骤&#xff0c;有需要者可以参考B站up我是土堆的视频&#xff09; 二&#xff1a;利用 conda…

本地生活直播,和电商直播有什么不一样?

直播正在成为零售业的标配&#xff0c;当下最新的一条赛道是“本地生活直播”。 &#xff08;商家开始在美团等平台进行本地生活直播。摄影&#xff1a;李崧稷&#xff09; 今年618&#xff0c;在老牌电商平台拉着无数网店&#xff0c;拼尽全力想要堆高销量的时候&#xff0c;一…

《TCP IP网络编程》第六章

《TCP IP网络编程》第六章&#xff1a;基于 UDP 的服务端/客户端 UDP 套接字的特点&#xff1a; 通过寄信来说明 UDP 的工作原理&#xff0c;这是讲解 UDP 时使用的传统示例&#xff0c;它与 UDP 的特点完全相同。寄信前应先在信封上填好寄信人和收信人的地址&#xff0c;之后…