1.前言
最近学习了vector类的用法和模拟实现,我们上一次提到的迭代器失效问题将在这一节进行讲解,如果对vector不了解的可以看我上一篇文章:
这一篇文章的要点:1.什么是迭代器失效?2.vector哪些操作会导致迭代器失效?3.如何避免迭代器失效?
2.什么是迭代器失效?
迭代器的作用:就是让算法不用关心底层的数据结构,不管你是数组还是链表,直接通过迭代器来访问,其底层就是指针,或者是对指针进行封装。
vector的迭代器:就是原生指针T*
cpp
template<class T>
typedef T* iterator; // 迭代器某种意义上就是指针
typedef const T* const_iterator;
因此迭代器失效:就是迭代器底层对应指针指向的空间被销毁了,而使用一块以及被释放的空间就是后果就是程序崩溃,具体的可以用一下三点来说明:
- 迭代器的本质就是指针,迭代器失效就是指针失效。
- 指针失效:指针指向的空间是非法的。
- 指针指向空间非法:指向的空间被释放 了,或者是越界访问。
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,我们看结果:
这里发生了断言错误

这段代码发生了两个错误:
- it是指向原空间的,当insert插入要扩容时,原空间的数据拷贝到新空间上了,这就意味着旧空间全是野指针,而it是一直指向旧空间的,随后遍历it时就非法访问野指针 ,迭代器也就失效了。形参的改变不会影响实参,所以即使内部的pos(it)改变了,外面还是原来的指针it,有人就要说我们把it++加一个判断条件,只有插入过后才会it++不就行了。这种在不同的编译器下有不同的结果,由于vs的检查很严格,只要前面发生了改变,后面的迭代器就都失效了,为了保证我们代码的可移植性,我们还需要别的解决办法。
- 为了解决上述问题,有人提出提前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++,对失效迭代器 的操作属于未定义行为。
当循环处理后续偶数元素(如 4、6)时,失效迭代器的非法操作会触发内存访问错误,最终导致断言失败(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迭代器失效有两种
- 扩容、缩容导致野指针式失效
- 迭代器指向的位置意义变了
系统越界机制检查,不一定能够检查到。编译实现检查机制,相对比较靠谱。