首页 > 编程语言 >[C++] vector && list 等容器的迭代器失效问题

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

时间:2024-06-16 16:28:15浏览次数:12  
标签:iterator 迭代 元素 list C++ v1 vector 失效

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

三、避免迭代器失效 


完~

未经作者同意禁止转载

标签:iterator,迭代,元素,list,C++,v1,vector,失效
From: https://blog.csdn.net/2301_79465388/article/details/139718972

相关文章

  • 【C++课程设计】通讯录管理系统
    完整代码:开源地址一、任务书 手机通讯录中的联系人的信息既可以存储在手机中,也可以存储在手机卡中,也可以同时存储在两个位置上(假设每个位置上的存储容量为1000,即手机卡中或手机上最多只能存储1000个联系人)。存储在手机中的联系人的信息只包含姓名和电话号码两项信息。存储......
  • C++题解—1140—亲密数对(东方博宜OJ)
    题目描述:键盘输入 N,N 在2至 2000之间,求 2 至 N 中的亲密数对,就是A的因子和等于B,B的因子和等于A ,且A≠B。如 48和 75是亲密数对。48的因子和为2+3+4+6+8+12+16+24=75 ,而 75的因子和3+5+15+25=48 。输入:只有一行,为一个整数 N( 2≤N≤2000 )。输出:输出......
  • C++双端队列deque源码的深度学习(stack,queue的默认底层容器)
    什么是deque?deque是C++标准模板库(STL)中的一个容器,代表“双端队列”(double-endedqueue)。deque支持在其前端(front)和后端(back)进行快速插入和删除操作,并且它在序列的中间插入和删除元素时通常比vector或list更高效。deque的特点双端插入和删除:你可以在deque的头部和尾部快速......
  • C++面向对象三大特性
    C++三大特性包括了封装、继承、多态。封装:封装是将数据属性和操作这些数据的函数(方法)捆绑在一起的过程。它隐藏了实现细节,只暴露出一个可以被外界访问的接口。封装允许开发者将对象的实现细节保护起来,只提供必要的操作界面,从而减少错误和提高代码的可维护性。继承:继承是一种......
  • Java-集合类-Arrays.asList()和subList使用需要注意的大坑
    Arrays.asList和subList使用需要注意的大坑一、Java-集合类-Arrays.asList()大坑1、不可修改列表大小&&原始数组与列表共享数据2、对于基本类型数组的使用限制两个错误案例wrong1wrong2二、Java-集合类-list.subList注意事项大坑1、ConcurrentModificationException2......
  • Qt/C++音视频开发77-获取本地有哪些摄像头名称/ffmpeg命令日志方式
    一、前言上一篇文章讲使用ffmpeg函数接口去获取本地摄像头信息,这种方式只能从ffmpeg5版本开始才具备,那ffmpeg3/4只能干瞪眼?那肯定不行的,必须要想办法打通这个功能,查阅信息发现可以执行命令ffmpeg-fdshow-list_devicestrue-idummy去获取,会通过日志打印出来,这是一个非常好......
  • C++U7-09-并查集
    并查集(DisjointSetUnion)是一种树型的数据结构,主要用于处理一些不相交集合(DisjointSets)的合并及查询问题。并查集能解决什么问题?在线游戏公会管理:应用场景:在一个大型多人在线游戏中,玩家可以创建或加入公会(公会相当于一个团队或群体)。随着时间的推移,公会可能会合并或解散。......
  • C++PrimerPlus:第十三章类和继承:静态联编和动态联编001
    第十三章类和继承:静态联编和动态联编提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加例如:静态联编和动态联编提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录第十三章类和继承:静态联编和动态联编前言一、指针和引用类型的兼......
  • C++前期概念(重)
    目录命名空间命名空间定义1.正常的命名空间定义2.命名空间可以嵌套 3.头文件中的合并 命名空间使用命名空间的使用有三种方式:1:加命名空间名称及作用域限定符(::)2:用using将命名空间中某个成员引入3:使用usingnamespace命名空间名称引入C++输入&输出说明:std......
  • python学习 - 对list列表的操作 实例代码
    #!/usr/bin/evnpython#-*-encoding:utf-8-*-list=[1,4,3,3,"A","B","c","A"]#增加list.append("AA")#像末尾增加一个新元素list.insert(1,"B")#像指定索引位置插入元素list.extend(["D","DD"])#新......