为啥有时迭代器用一下它就需要更新一下呢(迭代器失效)?

目录

前言

一、迭代器基本的底层设计

二、迭代器的失效原因

 1、删除  erase

解决方法:

 2、插入 insert push

总结


前言

为了规范STL中不同容器的统一的 访问方式,这里大佬们设计了 迭代器 来去替代C语言中指针的地位(在部分容器和部分平台上甚至直接就是指针的重命名中),所以迭代器的行为和指针的行为十分相似,但更加强大。

但有时这个灵活的小东西,在你删一个或者插入一个元素后,再用时它居然报错了,而且你看了后也没有语法错误,这个东西是被好几代大佬不断优化,所以一定是自己哪里写错了,那这是怎么回事呢?

一、迭代器基本的底层设计

在前言中,我们已经知道了迭代器的设计是对标的 指针 所以它的相关使用和规范也可以看齐指针,如:解引用 * 、基本的运算符······都是相同的。

但在部分容器中,由于一些原因 指针 的直接访问使用满足不了我们的需求,举一个典型的例子:链表(list),由于底层的物理地址不是连续的如果使用指针访问的话,相关的++、--、+、- 等运算符将不适用,但在C++中我们可以重载这些运算符来实现我们想要的目的,这也是迭代器对于 指针 改进地方,当然在不同的容器中迭代器的底层设计可能不同但是所有的迭代器的功能都是一样的,自此对于不同容器的访问方式,我们用来同一种东西实现了。

二、迭代器的失效原因

        在上一个小标题我们知道了迭代器就是对于指针的再封装和重载(即迭代器和指针一样指向的是地址),所以它本质上还是去调用我们 写好的正确方法 去访问正确的指针即地址,而它的本质既然是地址那内存这头肯定没有问题,那一定就是迭代器在指向地址时的问题了。

 1、删除  erase

注:尾删是一种 删除 中最特殊情况之一,为方便解释失效原理所以选择的,下文会引申到其它情况。且容器使用的为vector,其它容器可以类比。

void test_vector1()
	{
		std::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);

		//找到末尾的节点
		std::vector<int>::iterator it = find(v.begin(), v.end(), 4);

		if (it != v.end())
		{
            // it失效还是不失效?
			v.erase(it);
		}
//尾删后,能读写吗?
		// 读 
		cout << *it << endl;
		// 写
		(*it)++;

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}

        看完或运行代码后,你发现报错了,或者你会说这不报错才怪呢?这里的 it 迭代器指向的是最后一个节点,你把它都删了,你还访问别人不报错才有问题。

        那如果不是尾删呢?我们把上文的代码中删除的节点换为任意节点呢?如下修改

void test_vector2()
	{
		std::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);

		// it失效还是不失效?
		std::vector<int>::iterator it = find(v.begin(), v.end(), 3);
		if (it != v.end())
		{
			v.erase(it);
		}

		// 读 
		cout << *it << endl;
		// 写
		(*it)++;

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}

 此时可能会有不同的声音了,在部分平台上会报错如vs,而在部分平台上却可以正常运行如:g++

vs2013报错
g++正常运行

 

 有同学表示麻了,那这种情况到底是算错还是不算错呢?此时我们回到尾删的情况,在尾删时我们都知道一定会报错的,因为你访问了你已经删除的空间了,而其它位置的删除由于前移的原因,就算是改位置删除了,也有后面的节点来占位,所以我们就可以下结论了,尾删迭代器失效其它位置不会。

        哦,真的这么简单吗?    那位为啥在vs2013中只存在要删除了迭代器就会失效这种判定呢?

        我们再回到编程的思维,假定你在处理一个数组利用迭代器不停的翻来覆去的去不停的实现各种需求,此时每一次的操作都有可能去触碰尾节点,尾节点可以无数次在你的修改中保持不变,但只要有一次你删除了尾节点然后又去使用这就会报错,所以不仅为了平台的可移植性还为了代码的完美我们就要认定只要删除了节点迭代器就失效了

注:在不更新迭代器情况下,任意删除除了尾删会一定出错,也有其它的场景会得不到预期的结果

例如:1 2 2 5 vector容器中的需求删除偶数,结果会得到1 2 5,

原因:删除第一个2时由于删除节点后会自动前移,后it++,第二个2就被忽略了

void test_vector()
	{
		// 要求删除所有偶数
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		//v.push_back(5);


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

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}

解决方法:

在查询后,我们知道了erase会返回一个新的迭代器,这个返回值就是我们来更新旧迭代器的。

 当然如果不是被删的迭代器就不能用这种方法了,就只能老老实实的重新去找了,一般来说直接减去相应的删除个数就行。

注:由于空间的改变只针对被删的节点和该节点后面的节点(list链表由于节点的独立性,只会影响前者),所以只需要更新这些节点就行了

 

void test_vector3()
	{
		// 要求删除所有偶数
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		//v.push_back(5);


		vector<int>::iterator it = v.begin();
		while (it != v.end())
		{
			if (*it % 2 == 0)
			{
//更新迭代器
				it = v.erase(it);
			}
			else
			{
				++it;
			}		
		}

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}

 2、插入 insert push

在看完删除情况中的迭代器失效后,大家想必也知道了迭代器失效的原因在一些操作后旧迭代器会访问错误的地址,那此时我们就可以类比了。

删除是前移,插入就是后移了,当然也会改变地址迭代器与成员的对应关系,结局方法直接看删除的情况就行了。

注:特别的由于插入的情况还有一种特别的——扩容,当达到容器的容量时再插入,就会触发扩容机制,而扩容机制中又有方式异地扩容和本地扩容,参考前文尾删的存在就bug的情况,所以我们最好直接认为,每一次的插入都会触发扩容机制且都为异地扩容,所以每一次的插入都要更新迭代器,且是所有迭代器(都异地扩容搬家了,所有地址都变了),当然在部分容器中就不用更新,如list,它的节点都是独立的互不影响的


总结

迭代器是指针的优化版本,迭代器失效本质上是在一些操作后原本的迭代器会访问错误的,只要会地址空间结构改变的行为都要更新相应的迭代器。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值