前言
前几天面试过程中问到了这个问题,但回答的很模糊,如今得闲正好梳理总结一下。废话不多说直接进入正题。
一. STL标准库迭代器失效的原因
- 使用erase方法删除元素导致的迭代器失效
- 使用insert方法插入元素导致的迭代器失效
- 容器扩容导致的迭代器失效
二. 失效原因
谈到迭代器失效就不得不提到stl中的几种容器类别,STL的容器分为
- 序列式容器,如vector,deque(容器中的元素按照插入顺序存储,可以随机访问每个元素)
- 链表类容器,如list(容器中的元素通过指针链接在一起,不一定存储在一块连续的内存空间中)
- 关联类容器,如map, set,multimap,multiset(容器中的元素根据键值排序,并且不允许重复的键值存在。)
1. 序列式容器失效原因
(1) erase方法导致it迭代器从当前位置到容器末尾所有元素迭代器全部失效
int main()
{
vector<int> a{1, 2, 3, 4, 5};
for(vector<int>::iterator it=a.begin();it!=a.end();it++)
{
//去除容器内的所有奇数
if(*it%2==1)
a.erase(it);
}
for (auto elem : a)
cout << elem << endl;
return 0;
}
输出:报错
这是因为当我们删除it这个迭代器所指向的元素之后,it就变成了野指针。当it成为野指针之后我们对其进行++操作就会发生未定义行为,非法访问野指针,但是这个操作在release模式下不会报错,这是因为release模式下会对程序进行优化有概率不报错。那么其中还有一种重要的点就是:此类容器发生迭代器失效之后,从当前位置到容器末尾所有元素迭代器全部失效,因为erase之后后续所有元素都会“向前挤一挤”。
(2) insert方法导致it迭代器从当前位置到容器末尾所有元素迭代器全部失效
int main()
{
vector<int> a{1, 2, 3};
auto it = a.begin();
a.insert(a.begin(),0);
for(; it != a.end(); it++)
cout<<*it<<endl;
return 0;
}
输出:
-801756655
402654306
0
1
2
3
同理,插入一个元素之后,在插入位置之后的所有元素都要往后”挪一挪“,因为序列容器存储空间是连续的。那么还用原来老的it迭代器自然而然失效了。
(3) 解决方案
STL标准库中为insert和erase方法提供了返回值,会返回一个插入/删除位置之后的新迭代器,这样就避免了迭代器失效的问题。例程如下:
int main()
{
vector<int> a{1, 2, 3,4,5};
for(vector<int>::iterator it=a.begin();it!=a.end();)
{
if(*it%2==1)
//使用一个it接受返回的新的迭代器,实现自增
it = a.erase(it);
else
it++;
}
for (auto elem:a)
cout<<elem<<endl;
}
输出:
2
4
2. 链表式容器失效原因
对于链表式容器(如 list),删除当前的 iterator,仅仅会使当前的 iterator 失效,这是因为 list 之类的容器,使用了链表来实现,插入、删除一个结点不会对其他结点造成影响。只要在 erase 时,递增当前 iterator 即可,并且 erase 方法可以返回下一个有效的 iterator。
方式一:递增当前it
int main()
{
list<int> a{1, 2, 3,4,5};
for(list<int>::iterator it=a.begin();it!=a.end();)
{
if(*it%2==1)
a.erase(it++);
else
it++;
}
for (auto elem:a)
cout<<elem<<endl;
}
此时 a.erase(it++);所经历的操作分三步走,先把 iter 传值到 erase 里面,然后 iter 自增,然后执行 erase,所以 iter 在失效前已经自增了。故可行
方式二:使用erase/insert返回的迭代器
int main()
{
list<int> a{1, 2, 3,4,5};
for(list<int>::iterator it=a.begin();it!=a.end();)
{
if(*it%2==1)
//使用一个it接受返回的新的迭代器,实现自增
it = a.erase(it);
else
it++;
}
for (auto elem:a)
cout<<elem<<endl;
}
3. 关联式容器失效原因
对于关联容器(如 map, set,multimap,multiset),删除当前的 iterator,仅仅会使当前的 iterator 失效,只要在 erase 时,递增当前 iterator 即可。这是因为 map 之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响,但是原来的it会过期。
P.S.:其中map的erase 和insert会返回一个pair :
- 第一个元素 (pair.first) 是一个迭代器,指向新插入的元素或尝试插入的元素的位置。
- 第二个元素 (pair.second) 是一个布尔值,表示插入是否成功。如果插入成功,该值为 true;如果插入失败(因为键值已存在),则该值为 false。
而我们则要采用erase(iter++)的方式来遍历容器防止非法访问野指针。
for (iter = dataMap.begin(); iter != dataMap.end(); )
{
int nKey = iter->first;
string strValue = iter->second;
if (nKey % 2 == 0)
{
map<int, string>::iterator tmpIter = iter;
iter++;
dataMap.erase(tmpIter);
//dataMap.erase(iter++) 这样也行
}else
{
iter++;
}
}
4. 容器扩容导致的迭代器失效
如果容器扩容,在其他地方重新又开辟了一块内存。原来容器底层的内存上所保存的迭代器全都失效了。
解决方案
在扩容后重新获取一对迭代器就好了,谨慎使用扩容前的迭代器。