1.首先从一到题目开始谈说起迭代器失效。有时我们很自然并且自信地 用下面方法删除vector元素:
#include <iostream>
#include <stdio.h>
#include <vector>
#include <algorithm>
#include <string>
void del_elem(vector<string> &vec, const char * elem) { vector<string>::iterator itor = vec.begin(); for (; itor != vec.end(); itor++) { if (*itor == elem) { vec.erase(itor); } } } template <class InputIterator> void show_vec(InputIterator first, InputIterator last) { while(first != last) { std::cout << *first << " "; first++; } std::cout << " " << std::endl; } int main(void) { string arr[] = {"php", "c#", "java", "js", "lua"}; vector<string> vec(arr, arr+(sizeof(arr)/sizeof(arr[0]))); std::cout << "before del: " << std::endl; show_vec(vec.begin(), vec.end()); del_elem(vec, "php"); std::cout << "after del: " << std::endl; show_vec(vec.begin(), vec.end()); return 0; }
当 string arr[] = {"php", "c#", "java", "js", "lua"}; 时,运行上边程序,得到如下输出:
运行结果是正确的啊。 找到 "php" ,然后删除,剩下四个元素。
但是实际上 del_elem 的过程是和我们想象的不一样的,在 del_elem中打印下每一步的 itor 的值,就会发现蛛丝马迹。
将 del_elem加上log:
void del_elem(vector<string> &vec, const char * elem) { std::cout << "----------------------------" << std::endl; vector<string>::iterator itor = vec.begin(); for (; itor != vec.end(); itor++) { std::cout << *itor << std::endl; if (*itor == elem) { vec.erase(itor); } } std::cout << "----------------------------" << std::endl; }
我们在做删除操作前,打印每个元素的值, 继续编译运行得到如下结果:
在做 del_elem操作时,少打印了一个 "c#", 也就是在打印完"php",然后删除php以后,接下来打印的不是 "c#", 而直接打印了 "java" 。
那么我们可以将 vec.erase(itor) 注释掉,然后 可以得到 del_elem 会打印所有的元素值,
如此看来 c# 是因为执行了erase 操作以后,“变没了”。
弄清这个问题,我们要看看一组vector操作的定义:
iterator erase(iterator position) { if(position + 1 != end()) copy(position + 1, finish, position); --finish; destroy(finish); return position; }
iterator begin() { return start; }
iterator end() { return finish; }
我们经常使用 vec.begin(), vec.end(), 想必也能知道start和finish 为何物。
首先看erase函数: 先判断 待删除的迭代器是否为 finish 的前一个元素,也就是vector中的最后一个元素,
如果不是最后一个元素,就将待删除迭代器的后边所有元素往前移动一个slot, 然后 --finish 重新定位finish指针。
此时finish指针指向的是没删除之前的最后一个元素地址,本例中就是 lua的地址, 但是当前的finish指针处的值已经没用了,于是调用destroy。
如果待删除迭代器是finish的前一个元素的话,那么就直接移动finish指针,调用destroy销毁该位置处的元素对象。
与此同时,我们看到erase函数传进来的迭代器,只起到了一个位置定位判断的作用,erase本身没有对该迭代器有任何操作,该迭代器的值所在地址仍然有效,但是由于进行了copy操作,position处的值已经变成了"c#".
再回过头来看一下我们的 del_elem 函数:
当删除第一个元素php之后,在执行 itor++之前,php之后的所有元素都前移了一个slot导致此时 itor存放的元素已经是 c#,
于是继续执行itor++后,此时itor又向后移动,itor中的值已经是java,c#就是这样被漏掉的。
1-2 由此,又可以得出另一个结论,当arr中有n(n>=2)个 连续的php元素,我们用 del_elem函数 是不能删除掉所有的php元素的,于是这样就会导致bug。
我们将 string arr[] = {"php", "c#", "java", "js", "lua"}; 改为 string arr[] = {"php", "php", "php", "php", "c#", "java", "js", "lua"}; 后,观察运行结果:
固然不出所料,php没有被删除干净,因为当删除第一个php以后,用当前 del_elem 方法,总是会漏掉删除的php之后的元素,如果这个元素恰好是 "php",便会出现bug。
1-3 用当前 del_elem删除一个元素,会导致 finish 前移一个slot,如果将php放到最后slot,即finish之前的slot中,当删除最后一个php后,finish会指向删除的php的地址(已经非法了),
然后php的地方会被销毁,然后又执行 itor++,于是此时的itor指向到finish的后边,当 判断 itor != vec.end() 时,这个式子是成立的,于是继续执行,但是当对迭代器解引用时,最终会由于是非法
引用地址,程序崩掉。我们来看一下是否是这样, 将最后一个元素改为 "php"; string arr[] = {"php", "php", "php", "php", "c#", "java", "js", "php"};
编译运行,结果如下:
gdb调试,发现是因为 *itor 导致程序崩溃。
2 以上例子指出了vector 删除元素时选择的方法不当导致的一些问题;
1 是删除多个相同元素时,因为vector自身的特性导致 删除不净,出现bug
2 是当删除的元素时最后一个元素,可能导致程序崩溃。
3 我们很多时候都知道vector 迭代器失效会出问题,但是很多时候不知道会导致什么问题。
以上例子列举了 迭代器失效的 结果, 那么反过来 ,我们再研究 “什么是vector删除元素会导致迭代器失效” 的问题。
我的结论是,在对vector进行删除元素的时候, 删除元素之前,假设我们定义了一些迭代器分别指向,
1 迭代器的位置位于 待删除迭代器之前,
2 待删除的迭代器
3 迭代器的位置位于 待删除的迭代器之后
那么当对待删除的迭代器调用erase(itor)以后,之前定义在itor之前的迭代器依旧有用, 之前定义的 itor 以及 itor之后的迭代器 已经失效了,这里的失效是指,这些迭代器所指的元素内容已经和删除之前的不一样了,甚至可能是指向了非法地址。
于是在对这些失效的迭代器进行操作的时候 可能导致程序出bug ,或者直接崩溃。
4. 那么该如何删除vector元素呢?
可以参考:
void del_elem(vector<string> &vec, const char *elem) { vector<string>::iterator itor = vec.begin(); for (; itor != vec.end();) { std::cout << *itor << " " << &(*itor) << std::endl; if (*itor == elem) { itor = vec.erase(itor); //个人觉得这句赋值是多余的,因为erase本身没有对itor进行任何操作,erase操作之前和操作之后的itor所指向的位置是不变的,变的只是里边的值。 如有理解错误,还望及时指出 } else { itor++; } } }
5. 想必读者已经对vector删除元素引起的迭代器失效有了一些理解,那么再来理解插入元素导致的迭代器失效会更容易一些。
1 如果插入操作引起了空间重新配置,(申请新空间,赋旧空间的值到新空间,释放旧空间),那么在插入操作执行后,之前声明的所有迭代器都将失效
2 如果没有引起空间配置,那么会导致插入位置之后的迭代器失效。
6. 我们 假如我们声明了一些迭代器,对vector进行了插入或删除操作以后,要注意这些迭代器可能已经失效。
7. 由vector的迭代器失效,可以引出,其他序列式容器的迭代器失效,其他关联式容器的迭代器失效。内容太多,本篇只是先给出vector的迭代器失效的一些理解,后续继续补充其它的。
<effective stl> 第九条,较详细的讨论的各种容器的操作方法,有兴趣的读者可自行翻阅。
水平有限,错误难免,望及时指出。希望能对大家理解迭代器失效提供一些思路。