今天我们来看看迭代器失效的主要原因与解决方法
首先我们要了解到,迭代器对于vector阶段来说,我们完全可以把它看成一个指针来理解。
我们先来简单实现一下:
首先有一个重定义:
typedef T* iterator ;
就是定义一个指针类型
iterator begin()
{
return _start;
}
这不就是一个简单的迭代器吗?
当然这是一个普通容器,当然还有const版本,我们这里不深入说了。
我们在正式开始主题前,先看一个小问题:
为什么我们迭代器进行比较的时候,不用>、<而是 == 和!=。
就像这样:
当然在string和vector这种线性物理存储的结构上,我们这样写当然没问题,但是对于list和map等链式结构时,我们无法进行线性的比较。
当然迭代器的反向迭代器、适配器等问题,我们在之后的文章中产出。他们都是非常重要的概念。
我们再来看看vector的两个成员函数
insert和erase,我们今天的问题将由他们两个展开
我们可以看到,这两个函数的参数都接收的是迭代器变量,还有迭代器区间。
那么我们试用一下这两个函数,我们来进行头插
v.insert( v.begin( ), 0 );
头删操作完全一样。
这里我们需要注意一下,在vector中,我们很少用到这两个函数,尤其是在需要操作前半段数据的时候,需要一个一个挪动数据,效率是很低的。O(n)的时间复杂度;
那我们知道头插头删,那需要删除插入到中间呢?
这个时候,我们知道首先要使用迭代器进行插入删除操作,那迭代器只有begin和end,该怎么办呢?
vector和list没有提供查找的成员函数。
他们把查找的成员函数放在一个算法头文件中,它是用模板来实现的,为的是让所有数据结构都能用这个算法。
我们看到find这个函数非常的好用,给一个迭代器区间,在给一个你想要查找的值,就可以给你返回找到的迭代器了。
这里迭代器区间我们要了解的是,他一定是左闭右开的区间。
我们这里具体实现一下,比如我们要在3之前插入30;
vector : : iterator pos = find( v.begin() , v. end() , 30)
if ( pos != v.end() )
{
v.insert ( pos , 30);
}
聪明的你有看出来问题吗?
没有的话,我们再来看看删除操作
我们现在需要删除掉3,下面的操作是否可行呢?
v.erase ( pos) ;
pos这个位置不是之前找到的3的位置吗?
这样删除应该美神我们问题吧。
这里我们显而易见的是,我们把3之前的数30删除了。
这时候小伙伴们一定发现问题了,这里删除的pos位置在我们插入3之后,并没有改变,指向了原来3的空间,现在30的空间。所以我们很容易这样误操作,所删非所想。
这其实就是一种迭代器失效了。
我们首先给他起个名字:迭代器意义改变引起的迭代器失效。
这是我们要说的第一种失效,那应该怎么修复这样的问题呢?
也很容易,我们要删除的时候,再次调用find函数,找到我们要删除的新的迭代器,再去删除就好了。
下来我们看看第二种迭代器失效是什么样的。
我们先来告诉大家答案:
当我们增容的时候,原空间已经满了,而我们再去insert时自动调用reserve函数扩容,它会新创建一块新空间,释放掉旧的空间,把内容在指向新空间。
然而我们再次erase操作的时候,就会对原来的迭代器(这时候pos已经指向被释放的空间)野指针,当我们delete[ ]野指针,那你说危险不危险。
用一个例子说明一下:
我们来删除vector中的偶数
vector : : iterator it = v.begin( );
while( it != v.end ( ) )
{
if(*it %2 ==0)
v.erase ( it );
++it;
}
我们会这样写我们的代码,如果你也是,那下面请认真看了。
我们首先要知道,erase一个元素,剩余的元素会前补,it会指向补过来的元素。那我们++it的时候,难免会跳过一些元素,使之没有判断略过了,当然我们无法确定略过的是奇数还是偶数,如果我们把奇数略过了那还好,如果把偶数略过了,那就是一个错误代码。总而言之,这份代码是有问题的。
这里就是我们所说的第一种迭代器失效,迭代器的意义发生了改变。这种情况还好啊,当你的结果中出现一个偶数的时候,你会发现这里的问题的。
但这道题远远没有那么简单
我们假设一种情况,当这个vector中的元素是这样的
1 2 3 4 5 6
前面的逻辑很简单,就是一直循环判断。
我们重点关注最末尾的这个偶数,
6满足if条件,删除6,最后有end()前补,此时it迭代器指向的真实内容其实是end()了,然而,it++了。那这时候it是一个野指针了。
这时候while循环永远无法停下,导致了很大的问题。
这里也就是我们的第二种迭代器失效,野指针的问题,其中在增容和缩容的情况下,很容易产生这种问题。
当然如果有这样的操作,你的编译器会帮你报错,不论你是linuxg++还是vs用户。
这里需要注意一下,当我们在vs编译环境下,我们不论最后是奇数还是偶数都会直接进行报错,这是因为erase之后 it迭代器++的时候,编译器会帮我们检查。这也是vs的特性。
而在g++环境下,我们在末位是奇数的时候,程序是完美运行的。当是偶数时,才会进行报错。
这是两种不同环境带来的细节差异。
既然把问题都给出来了,也要把解决方案给出来。
我们如何解决迭代器产生野指针的问题呢?
我们可以接收erase 的返回值,这样一切问题都解决了。
像是这样: it = v. erase (it);
为什么呢?
我们要知道erase的返回值是什么样的?
返回新位置的迭代器,在我们这里就是删除元素下一个元素的迭代器。这样迭代器就被控制住了,一切问题都解决了。
最后我们再预告一点swap 的内容
swap也是一个algorithm中的函数,用的时候一定要加上头文件。
其中有两个版本:
C++98版本很明显是不好的,它完成一次交换需要进行三次拷贝。这效率是非常慢的。
C++11就解决的了这个问题,这个我们会在C++11新特性中讲解。