【C++】vector的迭代器失效问题,(什么是迭代器失效,为什么会产生迭代器失效,怎么避免迭代器失效问题)

1.前言

最近学习了vector类的用法和模拟实现,我们上一次提到的迭代器失效问题将在这一节进行讲解,如果对vector不了解的可以看我上一篇文章:

vector的模拟实现和各接口

这一篇文章的要点:1.什么是迭代器失效?2.vector哪些操作会导致迭代器失效?3.如何避免迭代器失效?

2.什么是迭代器失效?

迭代器的作用:就是让算法不用关心底层的数据结构,不管你是数组还是链表,直接通过迭代器来访问,其底层就是指针,或者是对指针进行封装。

vector的迭代器:就是原生指针T*

cpp 复制代码
template<class T>
typedef T* iterator;                     // 迭代器某种意义上就是指针
typedef const T* const_iterator;

因此迭代器失效:就是迭代器底层对应指针指向的空间被销毁了,而使用一块以及被释放的空间就是后果就是程序崩溃,具体的可以用一下三点来说明:

  1. 迭代器的本质就是指针,迭代器失效就是指针失效
  2. 指针失效:指针指向的空间是非法的。
  3. 指针指向空间非法:指向的空间被释放 了,或者是越界访问。

3.哪些操作会导致迭代器失效?

①:所有可能会引起扩容的操作 都可能会导致迭代器失效。如:resize,reserve,insert,assign,push_back等-------野指针访问引起的迭代器失效

②:指定位置的插入和删除都可能会引起迭代器失效。如:erase,insert---------迭代器指向的位置意义发生改变

4.如何避免迭代器失效?

接下来将从insert和erase两个接口来讲解如何避免迭代器失效

insert迭代器失效

insert迭代器失效分为两类:

  • 扩容造成野指针访问
  • 迭代器指向的位置意义发生改变

我们先给出insert的最初版本,我们在vector模拟实现中实现的代码,之后再逐渐完善:

cpp 复制代码
void insert(iterator pos,const T& val)
{
	//检验参数合法性
	assert(pos >= _start && pos <= _finish);
	//检测是否扩容
	if (_finish == _end_of_storage)
	{
		//size_t len = pos - _start;//避免后面的迭代器失效
		reserve(capacity() == 0 ? 4 : 2 * capacity());//reserve过后原来的start去新的地方了
		//pos = _start + len;//这里是更新后的start
	}
	iterator it = _finish - 1;//最后一个数据位置
	while (it >= pos)
	{
		*(it + 1) = *it;//往后挪
		it--;
	}
	*pos = val;
	++_finish;
}

迭代器失效------扩容造成的野指针访问

我们给出测试用例:

1.尾插4个数据后调用insert

2.尾插5个数据后调用insert

  • 为什么尾插4个过后再insert会出现随机值,而尾插5个后调用insert就没有这个问题?
  • 这个问题就是迭代器失效,原因就在于pos没有更新,导致野指针访问。
  • 上述尾插四个数据后,再调用insert函数,会发生扩容,根据reserve扩容机制,_start和_finish都会更新唯独插入的pos没有更新,这就导致pos依然指向旧空间 (_原来的start),reserve后会释放原来的空间,此时pos就是野指针,此时再执行***pos=val**,就是对野指针进行解引用了。

解决方法

我们可以设定变量len来计算扩容前pos指针位置和_start指针的相对距离len=pos-_start(两个指针相减结果是相差多少个元素) ,最后在扩容后,让更新后的_start再加上前算好的相对距离len就是更新后的pos指针指向的位置了

cpp 复制代码
void insert(iterator pos,const T& val)
{
	//检验参数合法性
	assert(pos >= _start && pos <= _finish);
	//检测是否扩容
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;//避免后面的迭代器失效
		reserve(capacity() == 0 ? 4 : 2 * capacity());//reserve过后原来的start去新的地方了
		pos = _start + len;//这里是更新后的start
	}
	iterator it = _finish - 1;//最后一个数据位置
	while (it >= pos)
	{
		*(it + 1) = *it;//往后挪
		it--;
	}
	*pos = val;
	++_finish;
}

迭代器失效-------迭代器指向的位置意义发生改变

什么是迭代器指向的位置意义发生改变呢?举个例子

比如我要在所有偶数前面插入2,我们看结果:

这里发生了断言错误

这段代码发生了两个错误:

  1. it是指向原空间的,当insert插入要扩容时,原空间的数据拷贝到新空间上了,这就意味着旧空间全是野指针,而it是一直指向旧空间的,随后遍历it时就非法访问野指针迭代器也就失效了。形参的改变不会影响实参,所以即使内部的pos(it)改变了,外面还是原来的指针it,有人就要说我们把it++加一个判断条件,只有插入过后才会it++不就行了。这种在不同的编译器下有不同的结果,由于vs的检查很严格,只要前面发生了改变,后面的迭代器就都失效了,为了保证我们代码的可移植性,我们还需要别的解决办法。
  2. 为了解决上述问题,有人提出提前reserve开辟足够大的空间就不会发生扩容了,也就可以避免野指针的现象,但是这样会造成一个新的问题:

此时insert以后虽然没有扩容,也没有野指针,但是it指向的位置意义变了,导致我们这个位置一直插入20

  • 迭代器指向的 "位置" 没变,但元素变了 :插入后,it物理地址未变(未扩容时),但指向的元素从 "原偶数" 变成了 "新插入的 20",导致重复判断插入。
  • 缺少 "跳过新插入元素" 的逻辑 :插入后需手动将it后移一位(it = v.insert(it, 20); ++it;),否则会重复处理新插入的元素。

解决方法:

给insert函数加上返回值,返回新插入元素的位置。

cpp 复制代码
//insert迭代器失效问题
iterator insert(iterator pos,const T& val)
{
	//检验参数合法性
	assert(pos >= _start && pos <= _finish);
	//检测是否扩容
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;//避免后面的迭代器失效
		reserve(capacity() == 0 ? 4 : 2 * capacity());//reserve过后原来的start去新的地方了
		pos = _start + len;//这里是更新后的start
	}
	iterator it = _finish - 1;//最后一个数据位置
	while (it >= pos)
	{
		*(it + 1) = *it;//往后挪
		it--;
	}
	*pos = val;
	++_finish;
	return pos;
}

我们实际调用时也需要改动,让it接收insert后的返回值

erase迭代器失效

我们先给出erase的初始版本,然后再逐渐完善:

cpp 复制代码
void erase(iterator pos)
{
	//检查合法性
	assert(pos >= _start && pos < _finish);
	//从pos + 1的位置开始往前覆盖,即可完成删除pos位置的值
	iterator it = pos + 1;
	while (it < _finish)
	{
		*(it - 1) = *it;//往前移动		
        it++;
	}
	_finish--;
}
  • erase的失效都是迭代器指向的位置意义发生改变,或者不在访问数据的有效范围内
  • erase一般不会采用缩容的方案,这就导致erase的迭代器失效一般不存在野指针的访问。

我们对下面代码进行测试:

cpp 复制代码
void test3()
{
	gjy::vector<int> v;
	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;
	auto it = v.begin();
	while (it !=v.end())
	{
		if (*it % 2 == 0)
		{
			v.erase(it);
			it++;
		}
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

本次代码中 vector 初始元素是 [1,2,3,4,5,6],循环逻辑是 "删除偶数元素":

  • it 指向 2(偶数)时,执行 v.erase(it)2 被删除,元素变为 [1,3,4,5,6],此时 it失效
  • 但代码紧接着执行 it++,对失效迭代器 的操作属于未定义行为

当循环处理后续偶数元素(如 46)时,失效迭代器的非法操作会触发内存访问错误,最终导致断言失败(Assertion failed: pos < _finish),程序因 abort() 调用而异常终止

我们再举另外一个例子说明:

cpp 复制代码
void test3()
{
	gjy::vector<int> v;
	v.push_back(10);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	auto it = v.begin();
	auto end = v.end();
	while (it !=v.end())
	{
		if (*it % 2 == 0)
		{
			v.erase(it);
		}
		it++;
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

当有连续的偶数时会漏掉偶数没有删除完。

代码出现了断言错误,我们画图解析

  • 被删元素后的所有元素会向前移动以保持内存连续;
  • 被删除的迭代器 it直接失效
  • 若删除的不是最后一个元素,其后续元素的迭代器也会失效。(vs严格检查跟上面insert一样,只在删除后it++在Linux g++编译器能跑通,但是vs不行)

解决方法:

给erase函数加上返回值即可,返回新插入元素的位置

cpp 复制代码
iterator erase(iterator pos)//这里不是void而是返回值,是因为vs检查机制很严格,只要删除过后,后面的迭代器都会失效,所以返回删除位置下一个元素的迭代器(其实--过后还是pos位置)
{
	assert(pos >= _start);
	assert(pos < _finish);

	iterator it = pos + 1;//记录后一个元素
	while (it < _finish)
	{
		*(it - 1) = *it;//往前面挪
		++it;
	}

	--_finish;

	return pos;
}

同样需要我们手动接收

5.迭代器失效总结

vector迭代器失效有两种

  • 扩容、缩容导致野指针式失效
  • 迭代器指向的位置意义变了

系统越界机制检查,不一定能够检查到。编译实现检查机制,相对比较靠谱。

相关推荐
卡比巴拉—林1 天前
Python print()函数详讲
开发语言·python
小π军1 天前
STL利器:upper_bound与lower_bound的使用
c++
奶思图米球1 天前
Python多环境管理
开发语言·python
JienDa1 天前
JienDa聊PHP:基于协同架构的PHP主流框架优势整合与劣势补救策略
开发语言·架构·php
i***39581 天前
JAVA系统中Spring Boot 应用程序的配置文件:application.yml
java·开发语言·spring boot
时光追逐者1 天前
C# 中 ?、??、??=、?: 、?. 、?[] 各种问号的用法和说明
开发语言·c#·.net·.net core
量化Mike1 天前
【python报错】解决卸载Python时报错问题:No Python installation was detected
开发语言·python
q***01771 天前
PHP进阶-在Ubuntu上搭建LAMP环境教程
开发语言·ubuntu·php
q***01771 天前
Java进阶学习之路
java·开发语言·学习
Zx623651 天前
13.泛型编程 STL技术
java·开发语言·c++