[C++] vector list 等容器的迭代器失效问题

标题:[C++] 容器的迭代器失效问题

@水墨不写bug



正文开始:

什么是迭代器?

        迭代器是STL提供的六大组件之一,它允许我们访问容器(如vector、list、set等)中的元素,同时提供一个遍历容器的方法。然而,在使用迭代器时,我们必须注意所谓的“迭代器失效”问题。

一、插入/删除元素

(1)erase导致删除位置之后的迭代器失效

        当我们在使用vector时,接收到了一组数据。然而这组数据的奇数具有实际意义,偶数需要被删除,这时候我们可能会写出这样的代码:

//删除偶数
vector<int> v1 = { 1,2,3,4,5,6,7,8,9,10,22,1};

for (vector<int>::iterator it = v1.begin();it!= v1.end();)
{
	if (*it % 2 == 0)
	{
		v1.erase(it);
	}
	else
	{
		it++;
	}
}

        乍一看,这段代码看似没有问题,并且在gcc上可以跑过,但是实际上这段代码是不符合C++标准的。

1.换一个编译器,比如VS2022,发现问题了:

 报错翻译过来就是迭代器不兼容。其实VS2022是通过对vector的迭代器进行封装来达到这一功能的。

2.在gcc上可以跑过,本质上只是一个巧合。


 迭代器失效本质

        在STL标准中,vector的erase会返回一个修正后的迭代器,而这样的修正就是为了避免迭代器失效。

        vector中的一组数据,{1,2,3,4,5,6,7,8,9};假设有一个迭代器指向元素5:

         在删除元素“5”后,由于会发生数据拷贝移动,于是这个迭代器就“顺势”指向了下一个元素“6”:

         这一行为是未定义的!如果我们使用的容器不是连续线性的,比如链表,那么结果将不堪设想,会产生野指针!

void test7()
{
	list<int> v1 = { 1,2,3,4,5,6,7,8,9,10,22,1 };
	for (list<int>::iterator it = v1.begin(); it != v1.end();)
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		else
		{
			it++;
		}
	}
	cout << v1;
}
int main()
{
	//test1();
	//test2();
	//test3();
	//test4();
	//test5();
	//test6();
	test7();
	return 0;
}

 


 vector的erase()原型:

iterator erase (const_iterator position);
iterator erase (const_iterator first, const_iterator last);

        STl的vector返回一个迭代器,指向被函数调用删除的最后一个元素后面元素的新位置。如果操作删除序列中的最后一个元素,则这是容器的末端。

        所以标准的写法是:


	vector<int> v1 = { 1,2,3,4,5,6,7,8,9,10,22,1};
	for (vector<int>::iterator it = v1.begin();it!= v1.end();)
	{
		if (*it % 2 == 0)
		{
			it = v1.erase(it);
		}
		else
		{
			it++;
		}
	}
	cout << v1;

这样就实时更新了迭代器。 


(2)insert导致的迭代器失效

i,插入元素之后位置的迭代器失效

        与删除元素之后位置的迭代器失效的问题本质上是一致的:

vector<int> v1 = { 1,2,3,4,5,6,7,8,9,10 };
vector<int>::iterator it = v1.begin() + 5;
cout << *it << endl;

v1.insert(v1.begin(), 4);
cout << *it << endl;

 在运行时报错:

 由于无法保留原来的迭代器,所以直接更新为指向插入元素的迭代器:

vector<int> v1 = { 1,2,3,4,5,6,7,8,9,10 };
vector<int>::iterator it = v1.begin() + 5;
cout << *it << endl;

it = v1.insert(v1.begin(), 0);
cout << *it << endl;

 

ii,扩容移动导致的迭代器失效

        我们看一段insert()的原型:

//在pos位置插入对象
iterator insert(iterator pos, const T& t)
	//由于可能需要扩容,会发生迭代器失效,对内部而言
	//迭代器pos在扩容前后指向的对象不再相同,对外部也是同样的会发生
{
	if (size() == capacity())
		//需要扩容
	{
		int len = pos - _start;
		int Newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(Newcapacity);
		//改变capacity,不改变size

		//记录len,解决迭代器失效的问题
		pos = _start + len;
	}

        通过分析,我们发现:在insert之前,会有一个是否需要扩容的检验,如果需要扩容,则释放旧空间,开辟新空间,然后拷贝数据。

        在这个过程中,如果需要扩容那么原来指向原旧空间的迭代器就失效了,如果访问失效的迭代器,会出现意想不到的结果。

        这个就解释了VS2022封装为什么迭代器。也许VS将迭代器封装为一个类,并且有这个类内部有一个判断迭代器是否失效的方法,如果我们访问了失效的迭代器就会报错。

二、容器重新分配内存

        其实,只要是导致容器的重新开辟这一动作时,就伴随着迭代器失效。

比如:

(1)std::vector 插入元素导致的重新分配

#include <iostream>  
#include <vector>  
  
int main() {  
    std::vector<int> v{1, 2, 3};  
    auto it = v.begin(); // 假设 it 指向第一个元素 1  
  
    // 插入元素,如果导致重新分配内存,it 将失效  
    v.push_back(4); // 如果 v 的容量不足以容纳新元素,它将重新分配内存  
  
    // 下面的代码在重新分配后可能会导致未定义行为  
    // *it = 0; // 如果 it 已失效,这是未定义行为  
  
    // 更好的做法是重新获取迭代器  
    it = v.begin(); // 现在 it 指向新的第一个元素(可能是原来的 1,也可能是新内存位置上的 1)  
  
    std::cout << *it << std::endl; // 输出:1  
    return 0;  
}

(2)std::vector resize 导致的重新分配

#include <iostream>  
#include <vector>  
  
int main() {  
    std::vector<int> v{1, 2, 3};  
    auto it = v.begin(); // 假设 it 指向第一个元素 1  
  
    // resize 到一个更大的大小,如果导致重新分配内存,it 将失效  
    v.resize(10); // 如果 v 的容量不足以容纳 10 个元素,它将重新分配内存  
  
    // 下面的代码在重新分配后会导致未定义行为  
    // *it = 0; // 如果 it 已失效,这是未定义行为  
  
    // 更好的做法是重新获取迭代器  
    it = v.begin(); // 现在 it 指向新的第一个元素(原来的 1 或新内存位置上的元素)  
  
    std::cout << *it << std::endl; // 输出:1  
    return 0;  
}

        这两个操作都导致了容器的重新开辟也就是 释放旧空间,开辟新空间进行容量调整的过程,所以造成迭代器失效。


三、避免迭代器失效 

        在实际应用中,我们要避免迭代器失效,就需要理解常见的错误及原理,养成良好的变成习惯,形成风格,这样才能在最大程度上减少错误!


目录

一、插入/删除元素:

(1)erase导致删除位置之后的迭代器失效

 vector的erase()原型:

(2)insert导致的迭代器失效

i,插入元素之后位置的迭代器失效

ii,扩容移动导致的迭代器失效

二、容器重新分配内存

(1)std::vector 插入元素导致的重新分配

(2)std::vector resize 导致的重新分配

三、避免迭代器失效 


完~

未经作者同意禁止转载

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

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

相关文章

【Perl】与【Excel】

引言 perl脚本语言对于文本的处理、转换很强大。对于一些信息量庞大的文本文件&#xff0c;看起来不直观&#xff0c;可以将信息提取至excel表格中&#xff0c;增加数据分析的可视化。perl语言的cpan提供了大量模块。对于excel文件的操作主要用到模块&#xff1a; Spreadshee…

Unity的三种Update方法

1、FixedUpdate 物理作用——处理物理引擎相关的计算和刚体的移动 (1) 调用时机&#xff1a;在固定的时间间隔内&#xff0c;而不是每一帧被调用 (2) 作用&#xff1a;用于处理物理引擎的计算&#xff0c;例如刚体的移动和碰撞检测 (3) 特点&#xff1a;能更准确地处理物理…

【算法】某赛车游戏中的组合计数问题及其扩展。推导思路:层层合并

文章目录 引言所有人都能完成可能有人未完成扩展问题参考资料 引言 在某款人称赛车界原神的赛车游戏中有组队竞速赛。共有n个人&#xff0c;n为偶数&#xff0c;分为人数相等的红队和蓝队进行比赛。结果按排名得分的数组为pts&#xff0c;单调递减且均为正整数。比如pts [10,…

算法day28

第一题 295. 数据流的中位数 本题我们是求解给定数组的中位数。且由于需要随时给数组添加元素&#xff0c;所以我们要求解该动态数组的中位数&#xff0c;所以本题最关键的就是维护数组在添加元素之后保持有序的排序&#xff0c;这样就能很快的求解中位数&#xff1b; 解法&am…

C++11完美转发(引用折叠、万能引用)

完美转发是指在函数模板中&#xff0c;完全依照模板的参数的类型&#xff0c;将参数传递给函数模板中调用的另外一个函数。 函数模板在向其他函数传递自身形参时&#xff0c;如果相应实参是左值&#xff0c;它就应该被转发为左值&#xff1b;如果相 应实参是右值&#xff0c;它…

web安全渗透测试十大常规项(一):web渗透测试之PHP反序列化

渗透测试之XSS跨站脚本攻击 1. PHP反序列化1.1 什么是反序列化操作? - 类型转换1.2 常见PHP魔术方法?- 对象逻辑(见图)1.2.1 construct和destruct1.2.2 construct和sleep1.2.2 construct和wakeup1.2.2 INVOKE1.2.2 toString1.2.2 CALL1.2.2 get()1.2.2 set()1.2.2 isset()1…

查看npm版本异常,更新nvm版本解决问题

首先说说遇见的问题&#xff0c;基本上把nvm&#xff0c;npm的坑都排了一遍 nvm版本导致npm install报错 Unexpected token ‘.‘install和查看node版本都正确&#xff0c;结果查看npm版本时候报错 首先就是降低node版本… 可以说基本没用&#xff0c;如果要降低版本的话&…

linxu-Ubuntu系统上卸载Kubernetes-k8s

如果您想从Ubuntu系统上卸载Kubernetes集群&#xff0c;您需要执行以下步骤&#xff1a; 1.关闭Kubernetes集群&#xff1a; 如果您的集群还在运行&#xff0c;首先您需要使用kubeadm命令来安全地关闭它&#xff1a; sudo kubeadm reset在执行该命令后&#xff0c;系统会提示…

【JavaEE进阶】——利用框架完成功能全面的图书管理系统

目录 &#x1f6a9;项目所需要的技术栈 &#x1f6a9;项目准备工作 &#x1f388;环境准备 &#x1f388;数据库准备 &#x1f6a9;前后端交互分析 &#x1f388;登录 &#x1f4dd;前后端交互 &#x1f4dd;实现服务器代码 &#x1f4dd;测试前后端代码是否正确 &am…

01 - matlab m_map地学绘图工具基础函数理解(一)

01 - matlab m_map地学绘图工具基础函数理解&#xff08;一&#xff09; 0. 引言1. m_demo2. 小结 0. 引言 上篇介绍了m_map的配置过程&#xff0c;本篇开始介绍下m_map中涉及到的所有可调用函数。如果配置的没有问题&#xff0c;执行">>help m_map"可以看到类…

【C++】C++入门的杂碎知识点

思维导图大纲&#xff1a; namespac命名空间 什么是namespace命名空间namespace命名空间有什么用 什么是命名空间 namespace命名空间是一种域&#xff0c;它可以将内部的成员隔绝起来。举个例子&#xff0c;我们都知道有全局变量和局部变量&#xff0c;全局变量存在于全局域…

趣味C语言——【猜数字】小游戏

&#x1f970;欢迎关注 轻松拿捏C语言系列&#xff0c;来和 小哇 一起进步&#xff01;✊ &#x1f389;创作不易&#xff0c;请多多支持&#x1f389; &#x1f308;感谢大家的阅读、点赞、收藏和关注&#x1f495; &#x1f339;如有问题&#xff0c;欢迎指正 感谢 目录 代码…

抖音混剪素材哪里找?可以混剪搬运视频素材网站分享

在抖音上制作精彩的视频离不开高质量的素材资源。今天&#xff0c;我将为大家推荐几个优质的网站&#xff0c;帮助你解决素材短缺的问题。这些网站不仅提供丰富的素材&#xff0c;还符合百度SEO优化的规则&#xff0c;让你的视频更容易被发现。 蛙学府素材网 首先要推荐的是蛙…

模拟自动滚动并展开所有评论列表以及回复内容(如:抖音、b站等平台)

由于各大视频平台的回复内容排序不都是按照时间顺序&#xff0c;而且想看最新的评论回复讨论内容还需逐个点击展开&#xff0c;真的很蛋疼&#xff0c;尤其是热评很多的情况&#xff0c;还需要多次点击展开&#xff0c;太麻烦&#xff01; 于是写了一个自动化展开所有评论回复…

诊断解决方案——CANdesc和MICROSAR

文章目录 一、CANdesc二、MICROSAR一、CANdesc canbeded是Vector汽车电子开发软件Nun Autosar标准的工具链之一。 canbeded是以源代码的形式提供的可重用的组件,包括CAN Driver,交互层(IL),网络管理(NM),传输层(TP),诊断层(CANdesc) , 通信测量和标定协议(CCP,XCP) 和 通信控…

Es 索引查询排序分析

文章目录 概要一、Es数据存储1.1、_source1.2、stored fields 二、Doc values2.1、FieldCache2.2、DocValues 三、Fielddata四、Index sorting五、小结六、参考 概要 倒排索引 优势在于快速的查找到包含特定关键词的所有文档&#xff0c;但是排序&#xff0c;过滤、聚合等操作…

并发容器(二):Concurrent类下的ConcurrentHashMap源码级解析

并发容器-ConcurrentHashMap 前言数据结构JDK1.7版本HashEntrySegment 初始化 重要方法Put方法扩容rehash方法 前言 之前我们在集合篇里聊完了HashMap和HashTable&#xff0c;我们又学习了并发编程的基本内容&#xff0c;现在我们来聊一聊Concurrent类下的重要容器&#xff0c…

tsp可视化python

随机生成点的坐标并依据点集生成距离矩阵&#xff0c;通过点的坐标实现可视化 c代码看我的这篇文章tsp动态规划递归解法c from typing import List, Tuple import matplotlib.pyplot as plt from random import randintN: int 4 MAX: int 0x7f7f7f7fdistances: List[List[in…

最长不下降子序列LIS详解

最长不下降子序列指的是在一个数字序列中&#xff0c;找到一个最长的子序列&#xff08;可以不连续&#xff09;&#xff0c;使得这个子序列是不下降&#xff08;非递减&#xff09;的。 假如&#xff0c;现有序列A[1&#xff0c;2&#xff0c;3&#xff0c;-1&#xff0c;-2&…

60.WEB渗透测试-信息收集- 端口、目录扫描、源码泄露(8)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;59.WEB渗透测试-信息收集- 端口、目录扫描、源码泄露&#xff08;7&#xff09; 御剑是用…