[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 导致的重新分配

三、避免迭代器失效 


完~

未经作者同意禁止转载

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

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

相关文章

2024上半年软考---江苏考区最先公布成绩

经历了考试之后&#xff0c;最期待的就是考试成绩的公布了&#xff0c;最好的成绩是45、45、45.只要过了分数线就满足了。下面我们来看看各大考区的分数的公布时间。 提前说下江苏考区的时间比较早&#xff0c;我就是江苏考区的&#xff0c;希望本次可以顺利通过考试。 2024年…

零基础到高手蜕变:一步到位Jupyter Notebook安装全攻略

前言 对于数据分析、机器学习、科学研究等领域的工作者来说&#xff0c;Jupyter Notebook 已经成为了一种不可或缺的工具。它的交互式编程界面&#xff0c;使得数据分析过程更加直观和高效。但并非所有人都熟悉如何安装和配置Jupyter Notebook&#xff0c;特别是在不同的操作系…

在typora中利用正则表达式,批量处理图片

一&#xff0c;png格式 在 Typora 中批量将 HTML 图片标签转换为简化的 Markdown 图片链接&#xff0c;且忽略 alt 和 style 属性&#xff0c;可以按照以下步骤操作&#xff1a; 打开 Typora 并加载你的文档。按下 Ctrl H&#xff08;在 Windows/Linux 上&#xff09;或 Cmd…

Unity C#调用Android,IOS震动功能

最近在Unity上需要很原生移动端进行交互&#xff0c; 原理&#xff1a;新建一个android项目&#xff0c;把生成的app module给干掉&#xff0c;然后留下一个vibrationPlugin module&#xff0c;在这个module下写android震动代码&#xff0c;将这个android工程构建出来的 aar移…

2024数据库期末综合解析(部分题)

目录 第4关&#xff1a;数据记录修改 任务描述 补充 答案&#xff1a; 第6关&#xff1a;数据查询二 任务描述 补充 答案&#xff1a; 第4关&#xff1a;数据记录修改 任务描述 湖南人口hnpeople数据表如下所示 各字段含义如下 cs&#xff08;城市)、qx(区县)、rk(人口)、man(男…

115.网络游戏逆向分析与漏洞攻防-邮件系统数据分析-调试优化结构体类型数据的创建

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果 现在的代码都是依据数据包来写的&#xff0c;如果看不懂代码&#xff0c;就说明没看懂数据包…

理解DDD设计

DDD的理解 领域驱动设计&#xff08;Domain-Driven Design&#xff0c;DDD&#xff09;是一种软件开发方法论&#xff0c;强调将业务领域作为软件设计的核心&#xff0c;以便更好地满足业务需求。DDD认为&#xff0c;软件开发的核心是理解业务&#xff0c;而不是实现技术。在D…

​晶体管高频等效电路

目录 混合Π等效电路 Y参数等效电路 混合Π与Y参数等效电路的转换 混合Π等效电路 共射三极管的等效电路。 Y参数等效电路 混合Π与Y参数等效电路的转换

异或运算的原理以及应用

异或&#xff08;XOR&#xff09;是计算机科学和数字电路中常用的运算之一。异或运算符通常用符号“⊕”或“^”表示&#xff0c;它有着简单而独特的性质&#xff0c;使其在数据加密、错误检测与纠正等多个领域得到了广泛的应用。在网络上我们传输的每一比特数据都经过了异或运…

unity 简易异步socket

1.unity 同步socket 改异步 using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Net.Sockets; using UnityEngine.UI; using System.Threading; using System;public class Echo : MonoBehaviour {//定义套接字Socket socket;//UG…

【C#】使用JavaScriptSerializer序列化对象

在C#开发语言编程中&#xff0c;通常使用系统内置的JavaScriptSerializer类来序列化对象&#xff0c;以便将其转换为JSON格式的文本存储与后台服务通信, 在这里将为大家详细介绍一下这个过程。 文章目录 反序列化序列化忽略属性 假设处理的数据中有一个对象类, 如下 public cl…

Linux系统脚本开机自启动,开机自启动jar包vue前台等

脚本内容jiaobenname.sh #!/bin/bash # 设置环境变量 export JAVA_HOME/usr/local/java/jdk-17.0.10 export CLASSPATH.:$JAVA_HOME/lib/ export PATH.:$JAVA_HOME/bin:$PATHwhile true; doif ps aux | grep -v grep | grep "tomcat" > /dev/null; thenecho &quo…

ppt添加圆角矩形,并调整圆角弧度方法

一、背景 我们看的论文&#xff0c;许多好看的图都是用PPT做的&#xff0c;下面介绍用ppt添加圆角矩形&#xff0c;并调整圆角弧度方法。 二、ppt添加圆角矩形&#xff0c;并调整圆角弧度 添加矩形&#xff1a; 在顶部工具栏中&#xff0c;点击“插入”选项卡。 在“插图”…

索引-定义、创建(CREATE INDEX)、删除(DROP INDEX)

一、概述 1、索引是SQL语言定义的一种数据对象&#xff0c;是大多数DBMS为数据库中基本表创建的一种辅助存取结构&#xff0c;用于响应特定查询条件进行查询时的查询速度&#xff0c;DBMS根据查询条件从数据库文件中&#xff0c;选择出一条或者多条数据记录以供检索&#xff0…

springboot优雅shutdown时异步线程安全优化

前面针对graceful shutdown写了两篇文章 第一篇&#xff1a; https://blog.csdn.net/chenshm/article/details/139640775 只考虑了阻塞线程&#xff0c;没有考虑异步线程 第二篇&#xff1a; https://blog.csdn.net/chenshm/article/details/139702105 第二篇考虑了多线程的安全…

基于C#开发web网页管理系统模板流程-参数传递

点击返回目录-> 基于C#开发web网页管理系统模板流程-总集篇-CSDN博客 前言 当用户长时间未在管理系统界面进行操作&#xff0c;或者用户密码进行了更改&#xff0c;显然用户必须重新登录以验证身份&#xff0c;如何实现这个功能呢&#xff1f; HTTP Cookie&#xff08;也叫 …

【Linux】 进程信号的发生

送给大家一句话&#xff1a; 何必向不值得的人证明什么&#xff0c;生活得更好&#xff0c;乃是为你自己。 -- 亦舒 进程信号的发生 1 何为信号2 信号概念的基础储备3 信号产生kill系统调用alarm系统调用异常core term Thanks♪(&#xff65;ω&#xff65;)&#xff89;谢谢…

【教程】设置GPU与CPU的核绑(亲和力Affinity)

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 简单来说&#xff0c;核绑&#xff0c;或者叫亲和力&#xff0c;就是将某个GPU与指定CPU核心进行绑定&#xff0c;从而尽可能提高效率。 推荐与进程优先…

1055 集体照(测试点3, 4, 5)

solution 从后排开始输出&#xff0c;可以先把所有的学生进行排序&#xff08;身高降序&#xff0c;名字升序&#xff09;&#xff0c;再按照每排的人数找到中间位置依次左右各一个进行排列测试点3&#xff0c; 4&#xff0c; 5&#xff1a;k是小于10的正整数&#xff0c;则每…

线程池ThreadPoolExecutor源码分析

一、线程池基本概念和线程池前置知识 1.1 Java中创建线程的方式有哪些 传统答案&#xff1a; 继承Thread类 通过继承Thread类并重写其run方法来创建线程。具体步骤包括定义Thread类的子类&#xff0c;在子类中重写run方法以实现线程的具体逻辑&#xff0c;然后创建子类的实例…