当前位置: 首页 > news >正文

C++:迭代器失效问题(vector为例)

希望文章能对你有所帮助,有不足的地方请在评论区留言指正,一起交流学习!

     本篇博客将剖析vector迭代器失效问题,以vector为例子 ,以insert和erase为例子讲解,插入图解进行分析。

目录

1. vector 迭代器是什么?

2.vector 迭代器失效

场景 1: “物理失效”—— 地址直接作废

场景 2: “逻辑失效”—— 地址有效但元素错位

3.如何避免使用迭代器失效

3.1.STL中的迭代器

3.2.vector模拟仿真的改进

4.总结


1. vector 迭代器是什么?

        vector 迭代器的底层是:原生指针的封装typedef T* iterator),本质是 “指向 vector 元素的内存地址”。

        也就是说,vector<int>::iterator本质就是int*vector<string>::iterator本质就是string*—— 它直接指向 vector 元素在内存中的地址。.

对比list

  • list 迭代器:是 “节点指针的封装”,每个节点独立存储,迭代器++时是跳转到下一个节点的地址(无需地址偏移);
  • vector 迭代器:是 “原生指针”,依赖连续内存的地址连续性

        这就决定了:list 迭代器只有在节点被删除时才失效,而 vector 迭代器会因为 “内存地址变化” 或 “元素移位” 频繁失效

2.vector 迭代器失效

        vector 迭代器失效分两种:物理失效”(地址无效)和 “逻辑失效”(地址有效但元素错位。两种失效的根源不同,但都会导致程序崩溃或数据错乱。

        场景的测试,我将采用之前模拟实现的vector来测试;因为在VS软件中,库中的vector迭代器失效会报错。C++:模拟实现vector的问题-CSDN博客。

场景 1: “物理失效”—— 地址直接作废

        vector 的容量(capacity)是固定的,当size == capacity时(元素装满了),再插入元素(如push_backinsert)会触发扩容

  1. 申请一块更大的连续内存(模拟实现为 2 倍扩容,初始容量为4);
  2. 把旧内存中的元素拷贝到新内存;
  3. 释放旧内存;
  4. 让 vector 的_start(首元素指针)指向新内存。

        而迭代器是指向 “旧内存” 的指针,旧内存被释放后,迭代器就变成了 “野指针”—— 访问野指针会导致未定义行为(程序崩溃、打印乱码等)。

        本小节包含push_back 扩容以及insert、erase的迭代器失效示例。(所有用到函数为模拟实现函数

触发操作:这些操作可能导致扩容

  • push_back(val):当size == capacity时;
  • insert(pos, val):当size == capacity时;
  • reserve(n):当n > 原capacity时(主动扩容)。

当然:erase也会导致地址直接作废的。删除尾部元素,迭代器指向尾部的时候。

vector<int> v;  // 初始capacity=0,size=0
auto it = v.begin();  // it指向空地址(如0x0)// 第一次push_back:capacity从0→1(扩容),新内存地址假设为0x200
v.push_back(1);  
// 此时it仍指向旧地址0x0(已被释放),但很多新手以为it还能用!
// cout << *it;  // 未定义行为!大概率崩溃

示例1 push_back函数 (野指针)

说明:下述程序,采用模拟实现的vector,初始容量为0,第一次扩容为4,第二次扩容为2倍,往后扩容依次为2倍。

// 测试迭代器失效
void test_vector4()
{vector<int> v;cout << "capacity:" << v.capacity() << endl;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);for (auto& e : v){cout << e << " ";}cout << endl;cout << "capacity:" << v.capacity() << endl; // 再次插入数据size()==capacity触发扩容vector<int>::iterator it = v.begin();v.push_back(5);cout << "capacity:" << v.capacity() << endl;while (it != v.end()){cout << *it << " ";it++;}cout << endl;
}

结果

扩容:

  1. 申请一块更大的连续内存(模拟实现为 2 倍扩容,初始容量为4);
  2. 把旧内存中的元素拷贝到新内存;
  3. 释放旧内存;
  4. 让 vector 的_start(首元素指针)指向新内存。

也就是说原来的迭代器已经失效了,指向的位置是随机值,不再归v管理了。

图解

示例2 insert函数 (野指针)

代码

// 测试迭代器失效
void test_vector4()
{vector<int> v;cout << "capacity:" << v.capacity() << endl;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);for (auto& e : v){cout << e << " ";}cout << endl;cout << "capacity:" << v.capacity() << endl; // 再次插入数据size()==capacity触发扩容vector<int>::iterator it = v.begin();v.insert(it, 5);cout << "capacity:" << v.capacity() << endl;while (it != v.end()){cout << *it << " ";it++;}cout << endl;
}

说明:只是将上述位置 push_back 改为 insert,也是导致扩容改变了_start。

        上述是代码调用的顺序,不会执行完成出程序 

运行结果

        “读取访问权限冲突” 是 C++ 程序中常见的内存错误,通常由非法内存访问导致。

分析原因

        越界访问了,会爆出错误

示例3 erase删除末尾元素,导致的迭代器失效。(野指针)

#include <iostream>
using namespace std;
#include <vector>
int main()
{int a[] = { 1, 2, 3, 4 };vector<int> v(a, a + sizeof(a) / sizeof(int));// 使用find查找3所在位置的iteratorvector<int>::iterator pos = find(v.begin(), v.end(), 4);// 删除pos位置的数据,导致pos迭代器失效。v.erase(pos);cout << *pos << endl; // 此处会导致非法访问return 0;
}

    vector 是连续内存容器,删除元素后,被删除元素的内存会被 “回收”(或被后续元素覆盖),指向该位置的迭代器自然失去有效性 —— 它不再指向 vector 中的任何有效元素。

        但是,删除最后一个元素,可以使用pop函数,更加高效。对于这种情况,需要重新利用end()更新迭代器。

总结:

        由于扩容或者删除尾部数据导致的的迭代器指针,指向的位置,无法被有效的访问,造成越界访问的,都是地址失效的野指针。

场景 2: “逻辑失效”—— 地址有效但元素错位

        删除元素时,内存没有被释放(不会触发扩容),但元素的位置变了,导致迭代器指向的 “内容” 不符合预期

为什么会失效?

        vector 是连续内存,删除pos位置的元素后,pos及之后的所有元素会向前移位,填补删除后的 “空位”。此时迭代器指向的地址虽然有效,但对应的元素已经不是原来的元素了 —— 逻辑上失效了。

触发操作:erase(pos) 或者insert函数

示例1 erase函数

int a[] = { 1, 2, 2, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
cout << "原始数据:";
for (auto& e : v)
{cout << e << " ";
}
cout << endl;auto it = v.begin(); // 删除所有的2
while (it != v.end())
{if (*it  == 2)v.erase(it);++it;
}
cout << "修改数据:";
for (auto& e : v)
{cout << e << " ";
}
cout << endl;

图解

        遍历的时候需要需要it++,但是erase会改变元素的位置,导致本应该删除的元素和指针it错过。因此改进的时候,可以增加一个返回值,返回pos找到的位置重新对比。

示例2 insert函数

// 测试迭代器失效
void test_vector4()
{vector<int> v;cout << "capacity:" << v.capacity() << endl;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);for (auto& e : v){cout << e << " ";}cout << endl;cout << "capacity:" << v.capacity() << endl; // 再次插入数据size()==capacity触发扩容vector<int>::iterator it = v.begin() + 2;//指向第三个元素的v.insert(it,10);//在第三个元素的位置插入一个10 此时it 指向的是10,但是//如果再次使用it 本意修改 3 但是it指向的是10,因此元素错位的情况cout << "capacity:" << v.capacity() << endl;cout << endl;
}

        insert没有比较合适的例子,本来指向3的迭代器最后指向了10.使用insert的返回值也没有用,需要使用begin函数更新it。

3.如何避免使用迭代器失效

3.1.STL中的迭代器

        在VS软件使用中,当迭代器失效出现的时候,程序会直接报错。

   示例1:insert插入数据

// 测试迭代器失效
void test_vector4()
{std::vector<int> v;cout << "capacity:" << v.capacity() << endl;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);for (auto& e : v){cout << e << " ";}cout << endl;cout << "capacity:" << v.capacity() << endl; vector<int>::iterator it = v.begin() + 2;v.insert(it, 10); // 会扩容cout << "capacity:" << v.capacity() << endl;cout << *it << endl;cout << endl;
}

  • 插入操作
    若插入位置在中间,插入后该位置及之后的迭代器全部失效;若触发扩容,所有迭代器失效
    解决:插入后通过 insert 的返回值获取新迭代器,或重新调用 begin()/end() 获取。

  • 删除操作
    删除位置及之后的迭代器全部失效。
    解决:用 erase 的返回值更新迭代器(如遍历删除元素时):

改进:库中的insert有返回值,增加了迭代器的更新

// 测试迭代器失效
void test_vector4()
{std::vector<int> v;cout << "capacity:" << v.capacity() << endl;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);for (auto& e : v){cout << e << " ";}cout << endl;cout << "capacity:" << v.capacity() << endl; vector<int>::iterator it = v.begin() + 2;it = v.insert(it, 10); // 会扩容cout << "capacity:" << v.capacity() << endl;cout << *it << endl;cout << endl;
}

        为 insert以及erase函数增加了返回值,这样就可以更新迭代器的地址了。

   示例2:erase删除数据

        删除数据导致的迭代器失效也会报错。

总结

        在VS中,容器状态改变后,必须确保迭代器指向有效,才能访问。当容器状态改变后,

只有通过 “用操作返回值更新迭代器” 或 “重新获取迭代器”,让迭代器绑定容器最新状态,才能安全使用。

3.2.vector模拟仿真的改进

        对于insert的改进有分为两步。

第一步 对于 it 指向为野指针

        insert插入的数据,导致扩容 pos变为野指针,_start和_finish都更新了,当然也要更新一下pos的指向了。这样防止pos变为野指针。

void insert(iterator pos, const T& x)
{//检测参数合法性assert(pos >= _start && pos <= _finish);//检测是否需要扩容/*扩容以后pos就失效了,需要更新一下*/if (_finish == _end_of_stoage){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++;
}

第二步 对于 it 指向导致的元素错位,增加返回值,更新迭代器。

iterator insert(iterator pos, const T& x)
{//检测参数合法性assert(pos >= _start && pos <= _finish);//检测是否需要扩容/*扩容以后pos就失效了,需要更新一下*/if (_finish == _end_of_stoage){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;
}

        这种改进的原因就是,在insert插入数据之后,vector作为连续的存储方式,it以及it之后的指针全部失效了,为保证it可以继续使用,更新一下it,想要原来it指向的元素,需要begin函数再次访问指向,或者修改it。(库中的做法)

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

4.总结

       “vector迭代器失效源于连续内存特性:扩容导致所有迭代器指向旧内存(物理失效),删除元素导致pos及之后迭代器逻辑错位(逻辑失效);需通过‘重新获取迭代器’或‘用erase返回值更新’规避,对比list可凸显连续内存与非连续内存的设计差异。”

如何在平时使用stl中避免迭代器失效报错:

1.如果执行了可能触发扩容的操作(如push_backinsert),不要复用之前保存的迭代器,必须通过begin()end()等接口重新获取。

2.erase(pos)有一个关键的返回值:指向 “被删除元素的下一个有效元素” 的迭代器。用这个返回值更新迭代器,就能避免逻辑失效。

3.迭代器的生命周期应该 “即用即取”,不要在循环外定义迭代器,再在循环内频繁修改 vector(如push_backerase)。

为什么 list 迭代器不容易失效?

很多人疑惑:同样是删除元素,为什么 list 迭代器很少失效?这还是要回到两种容器的底层结构差异:

  • list 是双向链表,每个元素是独立的节点,节点间通过prev/next指针连接,内存不连续;
  • 删除 list 的某个节点时,只需修改前后节点的指针(断开被删节点),其他节点的地址不变,因此只有被删除的迭代器失效,其他迭代器不受影响

        对比之下,vector 的连续内存特性决定了:任何导致 “内存地址变化” 或 “元素移位” 的操作,都可能引发迭代器失效 —— 这是 vector 高效随机访问的 “代价”。

        vector 迭代器失效的所有问题,都可以归结为一句话:迭代器是指向连续内存的指针,当内存地址变化(扩容)或元素位置变化(删除)时,指针要么作废,要么指向错误的内容

不要死记 “哪些操作会导致失效”,而是要理解:

  1. 迭代器的本质是原生指针;
  2. vector 的连续内存需要扩容和元素移位;
  3. 针对这两个特性,用 “重新获取迭代器”“用 erase 返回值更新” 等方法规避风险。


文章转载自:

http://YtXfioFm.wschL.cn
http://VMvCeMEi.wschL.cn
http://RpCeJDPU.wschL.cn
http://1SU0gSwb.wschL.cn
http://2WuRoCQp.wschL.cn
http://CKSSJwME.wschL.cn
http://gmUgktED.wschL.cn
http://ChrbTdYW.wschL.cn
http://3WTXC0RT.wschL.cn
http://kwC0HRCM.wschL.cn
http://0JofaRB9.wschL.cn
http://8LN969uD.wschL.cn
http://2esImotT.wschL.cn
http://8hZtOyEV.wschL.cn
http://cc47xmWI.wschL.cn
http://7NFuGIyt.wschL.cn
http://mmX3PspL.wschL.cn
http://4rpgOXa1.wschL.cn
http://JXXCFkEP.wschL.cn
http://DOwwYy3P.wschL.cn
http://V5kKlkxI.wschL.cn
http://i8dElljf.wschL.cn
http://yDHL37IC.wschL.cn
http://jbJLGOnK.wschL.cn
http://YIjUoWan.wschL.cn
http://ViMKDYzk.wschL.cn
http://BwOYmxUM.wschL.cn
http://MXMZVu8A.wschL.cn
http://n8fuuEni.wschL.cn
http://TSB7XpwQ.wschL.cn
http://www.dtcms.com/a/379450.html

相关文章:

  • TDengine 选择函数 TAIL() 用户手册
  • 在Linux系统中清理大文件的方法
  • oracle里的int类型
  • 【开关电源篇】整流及其滤波电路的工作原理和设计指南-超简单解读
  • 第五章 Logstash深入指南
  • 猫狗识别算法在智能喂食器上的应用
  • 数据库事务详解
  • Linux学习:基于环形队列的生产者消费者模型
  • size()和length()的区别
  • Windows系统下安装Dify
  • 企业云环境未授权访问漏洞 - 安全加固笔记
  • sv时钟块中default input output以及@(cb)用法总结
  • 广谱破局!芦康沙妥珠单抗覆罕见突变,一解“少数派”的用药困境
  • Guli Mall 25/08/12(高级上部分)
  • 彩笔运维勇闯机器学习--随机森林
  • Python 面向对象实战:私有属性与公有属性的最佳实践——用线段类举例
  • 使用deboor法计算三次B样条曲线在参数为u处的位置的方法介绍
  • 认识HertzBeat的第一天
  • AUTOSAR进阶图解==>AUTOSAR_EXP_ApplicationLevelErrorHandling
  • 线程同步:条件变量实战指南
  • OpenLayers数据源集成 -- 章节七:高德地图集成详解
  • AI助推下半年旺季,阿里国际站9月采购节超预期爆发
  • 电商平台拍立淘API接口调用全解析(基于淘宝/唯品会技术实践)
  • 9.11 Qt
  • 字节一面 面经(补充版)
  • 第二章 ELK安装部署与环境配置
  • I2C 总线
  • 设计模式——七大常见设计原则
  • 请创建一个视觉精美、交互流畅的进阶版贪吃蛇游戏
  • 利用美团龙猫添加xlsx的sheet.xml读取sharedStrings.xml中共享字符串输出到csv功能