C++——vector迭代器失效与深浅拷贝问题

news2024/11/24 2:20:31

目录

1. vector迭代器失效问题

1.1 insert迭代器失效

1.1.1 扩容导致野指针

1.1.2 意义变了

1.1.3 官方库windows下VS和Linux下对insert迭代器失效的处理

1.2 erase迭代器失效

1.2.1 失效原因分析

1.2.2 官方库windows下VS和Linux下对erase迭代器失效的处理

1.2.3 测试用例

1.3 迭代器失效总结

2. 深浅拷贝问题

1. vector迭代器失效问题

1.1 insert迭代器失效

上文我们写了insert的模拟实现,这里先我们给出不完善版本,以insert的雏形开始往后深层次递进演化,如下:

void insert(iterator pos, const T& x)
{
	//检测参数合法性
	assert(pos >= _start && pos <= _finish);
	//检测是否需要扩容
	if (_finish == _endofstoage)
	{
		size_t newcapcacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapcacity);
	}
	//挪动数据
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *(end);
		end--;
	}
	//把值插进去
	*pos = x;
	_finish++;
}

insert的迭代器失效分为两大类:

  1. 扩容导致野指针。
  2. 意义变了。

1.1.1 扩容导致野指针

我们给出两组测试用例如下:

怎么push_back尾插4个后调用insert会出现随机值?而push_back尾插5个后调用insert就没问题?

此问题就是迭代器失效,原因在于pos没有更新。导致非法访问野指针。

上述当尾插4个数字后,再头插一个数字,发生扩容,根据reserve扩容机制,_start和_finish都会更新,唯独这个插入的位置pos没有更新,此时pos依旧执行旧空间,再者reserve后会释放旧空间,此时的pos就是野指针,这也就导致后续执行*pos = x就是对非法访问野指针,所以最终结果就是随机值。

  • 解决办法:

可以通过设定变量n来计算扩容前pos指针位置和_start指针位置的相对距离,最后在扩容后,让_start再加上先前算好的相对距离n就是更新后的pos指针的位置了。

  • 修正如下:
void insert(iterator pos, const T& x)
{
	//检测参数合法性
	assert(pos >= _start && pos <= _finish);
	/*扩容以后pos就失效了,需要更新一下*/
	if (_finish == _endofstoage)
	{
		size_t n = pos - _start;//计算pos和start的相对距离
		size_t newcapcacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapcacity);
		pos = _start + n;//防止迭代器失效,要让pos始终指向与_start间距n的位置
	}
	//挪动数据
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *(end);
		end--;
	}
	//把值插进去
	*pos = x;
	_finish++;
}

此时的迭代器失效已经解决了一部分,当然还存在一个迭代器失效问题,见下: 

1.1.2 意义变了

比如现在我要在所有的偶数前面插入2,可是测试结果确是如下:

这里发生了断言错误,这段代码发生了两个错误:

  • 和上面的错误一样,首先it是指向原空间的,当insert插入到要扩容时,原来的旧数据被拷到了新空间上,这也就意味着旧空间全是野指针,而it一直是指向旧空间的,随后遍历it时就非法访问野指针,也就失效了。形参的改变不会影响实参,即使你内部pos的指向改变了,但是并不会影响我外部的it。
  • 为了解决上面的错误,有人会觉着提前reserve开辟足够大的空间即可避免发生野指针的现象,但是又出现了一个新的问题,看图:

 此时insert以后虽然没有扩容,it也没有成为野指针,但是it指向位置意义变了,导致我们这个程序重复插入20。

  • 解决办法:

给insert函数加上返回值即可解决,返回指向新插入元素的位置。

iterator insert(iterator pos, const T& x)
{
	//检测参数合法性
	assert(pos >= _start && pos <= _finish);
	//检测是否需要扩容
	/*扩容以后pos就失效了,需要更新一下*/
	if (_finish == _endofstoage)
	{
		size_t n = pos - _start;//计算pos和start的相对距离
		size_t newcapcacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapcacity);
		pos = _start + n;//防止迭代器失效,要让pos始终指向与_start间距n的位置
	}
	//挪动数据
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *(end);
		end--;
	}
	//把值插进去
	*pos = x;
	_finish++;
	return pos;
}

我们实际调用那块也得改动,让it自己接收insert后的返回值:

void test_vector10()
{
	//在所有的偶数前面插入2
	cpp::vector<int> v;
	//v.reserve(10);
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	v.push_back(6);
	cpp::vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			it = v.insert(it, 20);
			it++;
		}
		it++;
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
}

 

1.1.3 官方库windows下VS和Linux下对insert迭代器失效的处理

针对于扩容发生野指针类的迭代器失效,VS官方库是直接断言报错。

Linux这里可以直接访问,甚至是可以修改。可见不同环境下对待迭代器失效的处理方式是不一样的,windows下更加严格,Linux下比较佛系。

1.2 erase迭代器失效

1.2.1 失效原因分析

 erase模拟实现的代码:

iterator erase(iterator pos)
{
	//检查合法性
	assert(pos >= _start && pos < _finish);
	//从pos + 1的位置开始往前覆盖,即可完成删除pos位置的值
	iterator it = pos + 1;
	while (it < _finish)
	{
		*(it - 1) = *it;		
        it++;
	}
	_finish--;
	return pos;
}
  • erase的失效都是意义变了,或者不在有效访问数据的有效范围内;
  • 一般不会使用缩容的方案,那么erase的失效,一般也不存在野指针的失效。

现在要对如下代码进行测试:

void test2()
{
	cpp::vector<int> v;
	//v.reserve(10);
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	cout << v.size() << ":" << v.capacity() << endl;
	auto pos = find(v.begin(), v.end(), 2);
	if (pos != v.end())
	{
		v.erase(pos);
	}
	cout << *pos << endl;
	*pos = 10;
	cout << *pos << endl << endl;
	cout << v.size() << ":" << v.capacity() << endl;
	for (auto e : v)
	{
		cout << e << " ";
	}
}

这里首先在尾插4个数据后,比较了下size和capacity的大小,此时是相等的,接下来删除值为2的数,此时*pos就是删除数字的下一个数据,没有问题,并且s=有效数据size也少了一个,后续修改*pos也没有问题。

  • 可是当我要删除值为4的数据呢,再执行上述测试用例会是什么结果呢?

 这里我总共就有4个数字,按理说把最后一个数字删去后,有效数字-1,理应不存在说还会访问最后一个值的现象,但是此结果确实是删掉4后又访问了4,离谱的是还修改了4为10,这就是erase典型的迭代器失效。但是这里也不足为奇,因为你空间还没有缩容,删掉的4还存在,导致最终还能够被访问。

1.2.2 官方库windows下VS和Linux下对erase迭代器失效的处理

  • VS环境下检擦非常严格, 直接强制检擦断言错误。
  • Linux下对于迭代器失效的检查就宽泛很多,不会报错。
  1. erase(pos)以后pos失效了,pos的意义变了,但是在不同平台下面对于访问pos的反应是不一样的,我们用的时候要以失效的角度去看待此问题。
  2. 对于insert和erase造成迭代器失效问题,linux的g++平台检查很佛系,基本靠操作系统本身野指针越界检擦机制。windows下VS系列检擦更严格一些,使用一些强制检擦机制,意义变了可能会检擦出来。
  3. 虽然g++对于迭代器失效检查时是非常佛系的,但是套在实际场景中,迭代器意义变了,也会出现各种问题。

1.2.3 测试用例

void test4()
{
	//删除所有的偶数
	std::vector<int> v;
	//v.reserve(10);
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			v.erase(it);
		}
		it++;
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
}

 

 

void test4()
{
	//删除所有的偶数
	std::vector<int> v;
	//v.reserve(10);
	v.push_back(1);
	v.push_back(2);
	v.push_back(2);
	v.push_back(2);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			it = v.erase(it);
		}
		else
		{
			it++;
		}
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
}

1.3 迭代器失效总结

vector迭代器失效有2种

  • 1、扩容,缩容,导致野指针式失效
  • 2、迭代器指向的位置意义变了

系统越界机制检查,不一定能检查到。

编译实现机制检查,相对靠谱。

2. 深浅拷贝问题

 接下来用先前模拟实现的vector来测试杨辉三角以此来解释我们的深浅拷贝问题:

namespace cpp
{
	class Solution {
	public:
		// 核心思想:找出杨辉三角的规律,发现每一行头尾都是1,中间第[j]个数等于上一行[j-1]+[j]
		vector<vector<int>> generate(int numRows) {
			vector<vector<int>> vv;
			// 先开辟杨辉三角的空间
			vv.resize(numRows);
			for (size_t i = 1; i <= numRows; ++i)
			{
				vv[i - 1].resize(i, 0);
				// 每一行的第一个和最后一个都是1
				vv[i - 1][0] = 1;
				vv[i - 1][i - 1] = 1;
			}
			for (size_t i = 0; i < vv.size(); ++i)
			{
				for (size_t j = 0; j < vv[i].size(); ++j)
				{
					if (vv[i][j] == 0)
					{
						vv[i][j] = vv[i - 1][j - 1] + vv[i - 1][j];
					}
				}
			}
			return vv;
		}
	};
 
	void test7()
	{
		vector<vector<int>> vv = Solution().generate(5);
		for (size_t i = 0; i < vv.size(); ++i)
		{
			for (size_t j = 0; j < vv[i].size(); ++j)
			{
				cout << vv[i][j] << " ";
			}
			cout << endl;
		}
	}
}

 

 扩容代码如下:

//reserve扩容
void reserve(size_t n)
{
	size_t sz = size();//提前算出size()的大小,方便后续更新_finish
	if (n > capacity())
	{
		T* tmp = new T[n];
		if (_start)//判断旧空间是否有数据
		{
			memcpy(tmp, _start, sizeof(T) * size());
			delete[] _start;//释放旧空间
		}
		_start = tmp;//指向新空间
	}
	//更新_finish和_endofstoage
	_finish = _start + sz;
	_endofstoage = _start + n;
}

原因:

这里出错的原因在于扩容,错在扩容时调用的memcpy是浅拷贝,导致先前存储的数据被memcpy后再delete就全删掉变成随机值了。

仔细观察调用的这行代码:

vector<vector<int>> vv = Solution().generate(5);

这行代码的意义是有一个vector容器,其内部成员也是一个vector容器,就好比一个二维数组,有n行,每一行都是一个一维数组。画图演示上述测试用例的原因:

总结:

  1. vector<T>中,当T设计深浅拷贝的类型时,如:string/vector<T>等等,我们扩容使用memcpy拷贝数据是存在浅拷贝问题。
  2. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中。
  3. 如果拷贝的是自定义类型的元素,memcpy即高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。
  • 解决方案:

    reserve扩容时不使用memcpy,改成for循环来解决:

//reserve扩容
void reserve(size_t n)
{
	size_t sz = size();//提前算出size()的大小,方便后续更新_finish
	if (n > capacity())
	{
		T* tmp = new T[n];
		if (_start)//判断旧空间是否有数据
		{
			//不能用memcpy,因为memcpy是浅拷贝
			for (size_t i = 0; i < size(); i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;//释放旧空间
		}
		_start = tmp;//指向新空间
	}
	//更新_finish和_endofstoage
	_finish = _start + sz;
	_endofstoage = _start + n;
}

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

0基础学习VR全景平台篇第46篇:底部菜单- 【开场地图】与【高清矩阵】的对比

大家好&#xff0c;欢迎观看蛙色VR官方——后台使用系列课程&#xff01; 这期 &#xff0c;我们将为大家讲解蛙色VR平台-【开场地图】与【高清矩阵】功能的区别 功能位置示意 一、功能具体应用 开场地图分为两种&#xff0c;分别是高德地图和手绘地图。 高德地图点位目前系统…

【Linux】MySQL 存储引擎

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 MySQL 存储引擎 MyISAM 表支持 3 种不同的存储格式常用存储引擎&#xff1a;InnoDB、MyISAM查看系统支持的存储引擎查看表使用的存储引擎修改存储引擎死锁如何尽可能避免死锁…

每日学术速递6.7

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1.The ObjectFolder Benchmark: Multisensory Learning with Neural and Real Objects(CVPR 2023) 标题&#xff1a;ObjectFolder 基准测试&#xff1a;使用神经和真实对象进行多感官…

计算机网络管理- SNMP协议报文和报文格式分析,SNMP PDU分析

⬜⬜⬜ &#x1f430;&#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea;(*^▽^*)欢迎光临 &#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea;&#x1f430;⬜⬜⬜ ✏️write in front✏️ &#x1f4dd;个人主页&#xff1a;陈丹宇jmu &am…

【玩转Linux操作】Linux服务管理

&#x1f38a;专栏【玩转Linux操作】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【如愿】 大一同学小吉&#xff0c;欢迎并且感谢大家指出我的问题&#x1f970; 文章目录 &#x1f354;服务(service)管理⭐service管理指令 &…

《网络安全0-100》网络安全技术

网络安全技术 防火墙 入侵检测系统 防火墙(Firewall)是一种网络安全设备&#xff0c;用于监控和控制网络流量&#xff0c;以保护网络免受未经授权的访问和攻击。防火墙通过过滤网络流量&#xff0c;阻止潜在的攻击流量进入网络。它可以实现基于端口、协议、IP地址和应用程序的…

chatgpt赋能python:Python指定日期的后一天:介绍与解析

Python指定日期的后一天&#xff1a;介绍与解析 Python是广泛使用的编程语言之一&#xff0c;在数据科学、机器学习、人工智能和网络开发等众多领域都有重要的应用。本文将要讨论的问题是&#xff1a;如何使用Python来获取一个指定日期的后一天&#xff1f;这个问题看似简单&a…

QT找不到python27.dll或python36.dll解决办法

缺少python27.dll 点开QT时出现上述问题&#xff0c;一种方法就是找python27.dll文件进行替换。 1.进入网站下载相关文件 windll.com/dll/python-… 2.之后将文件移动到下面的路径里&#xff0c;C:\Windows\SysWOW64\ 3.然后重启 试了4种方案&#xff0c;把32位放在System32…

【操作系统】哲学界进餐问题实现

目录 实验原理&#xff1a; 实验目的&#xff1a; 实验数据及结果分析&#xff1a; 实验原理&#xff1a; 哲学家进餐问题可以通过限制同时进餐人数为4解决&#xff0c;为此需要设置一个值为4的互斥信号量sem_eaters,并且每根筷子只能同时由一个人使用&#xff0c;需要设置…

kafka的初步认识

什么是Kafka&#xff1f; Kafka的增长很快。财富500强企业中超过三分之一使用卡夫卡。这些公司包括十大旅游公司&#xff0c;十大银行中的七家&#xff0c;十大保险公司中的八家&#xff0c;十大电信公司中的九家&#xff0c;等等。LinkedIn&#xff0c;微软&#xff08;Micro…

【CV 向】如何打造一个“数串串神器“

导言 最近&#xff0c;我看到了这样一个视频&#xff0c;觉得很有意思&#xff0c;就随手保存下来了。&#x1f601;&#x1f601;&#x1f601; 之前吃串串火锅&#xff0c;老板数竹签不是称重就是用手慢慢数&#xff0c;但是称重似乎总是得不到正确的竹签数目&#xff0c;而…

C++技能系列 ( 7 ) - 右值引用、移动语意、完美转发

现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the future of dream weaving wings, let the dream fly in reality. 右值引用、移动语意、完美转发 1、右值引用2、完美转发 1、右值引用 右值引用&#xff08;rvalue refe…

Redux的纯函数、中间件

当我们的应用随着业务的发展&#xff0c;变得越来越复杂的时候&#xff0c;组件之间的状态也是越来越复杂。 reducer函数 之前说过redux中的action&#xff0c;是用来描述一种变化。但是完成变化并且生辰新的数据数据状态的是reducer方法。 reducer方法&#xff0c;必须是纯…

SQL注入攻击与防护

目录 一、SQL注入攻击概述 1.1 SQL注入概念 1.1.1 标准查询过程 1.1.2 SQL注入定义 1.2 SQL注入根本原因 1.3 SQL注入条件 1.4 SQL注入防范 1.4.1 根本原因&#xff1a;过滤不严 1.4.2 安全设计原则&#xff1a;数据与代码分离 1.5 SQL注入流程 1.6 SQL注入分类 1.…

vue3+vite+ts项目配置开发环境和生产环境 打包命令配置

开发环境和生产环境的配置和打包方式有所不同&#xff0c;下面是基于vue3vitets项目的开发环境和生产环境配置及打包方式的详细说明。 开发环境配置 开发环境的配置主要是为了方便开发者进行调试和测试&#xff0c;以下是开发环境的配置步骤&#xff1a; 1.1 安装依赖 首先…

DAY27:回溯算法(二)组合问题及其优化

文章目录 77.组合&#xff08;一定要注意逻辑问题&#xff09;思路for循环嵌套的情况回溯算法模拟for循环K层嵌套 回溯法步骤伪代码完整版debug测试逻辑问题&#xff1a;没有输出逻辑问题&#xff1a;为什么是递归传入i1而不是startIndex1&#xff1f;重要&#xff1a;为什么会…

Java-API简析_java.lang.CharSequence接口(基于 Latest JDK)(浅析源码)

【版权声明】未经博主同意&#xff0c;谢绝转载&#xff01;&#xff08;请尊重原创&#xff0c;博主保留追究权&#xff09; https://blog.csdn.net/m0_69908381/article/details/131318474 出自【进步*于辰的博客】 其实我的【Java-API】专栏内的博文对大家来说意义是不大的。…

Unreal 5 实现场景

如果你拿到了一个新的场景&#xff0c;想将此场景应用到游戏当中&#xff0c;首先需要给敌人增加ai移动路径&#xff0c;需要添加导航体积 添加导航模型包围体积 添加了体积以后&#xff0c;设置包围盒的大小&#xff0c;将敌人可以行进的区域给区分出来&#xff0c;然后按键盘…

PyCharm2023开发工具activice教程(包含工具link)

PyCharm2023 前言1. 下载工具2. 选择安装方法33. 填入active code4. 效果如下 前言 PyCharm是一款由JetBrains开发的强大的Python集成开发环境&#xff08;IDE&#xff09;。它提供了丰富的功能和工具&#xff0c;旨在提高Python开发者的生产力和效率。 以下是PyCharm的一些主…

Linux操作系统体系结构 ( 3 ) -【Linux通信架构系列 】

系列文章目录 C技能系列 Linux通信架构系列 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 期待你的关注哦&#xff01;&#xff01;&#xff01; 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the…