STL容器 迭代器失效总结

本文详细介绍了C++标准库中不同容器类型(如vector、deque、map等)在执行插入和删除操作时迭代器失效的情况,并提供了预防迭代器失效的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

迭代器(iterator)是一个可以对其执行类似指针的操作(如:解除引用(operator*())和递增(operator++()))的对象,我们可以将它理解成为一个指针。但它又不是我们所谓普通的指针,我们可以称之为广义指针,你可以通过sizeof(vector::iterator)来查看,所占内存并不是4个字节。

     1.首先对于vector而言,添加和删除操作可能使容器的部分或者全部迭代器失效。那为什么迭代器会失效呢?vector元素在内存中是顺序存储,试想:如果当前容器中已经存在了10个元素,现在又要添加一个元素到容器中,但是内存中紧跟在这10个元素后面没有一个空闲空间,而vector的元素必须顺序存储一边索引访问,所以我们不能在内存中随便找个地方存储这个元素。于是vector必须重新分配存储空间,用来存放原来的元素以及新添加的元素:存放在旧存储空间的元素被复制到新的存储空间里,接着插入新的元素,最后撤销旧的存储空间这种情况发生,一定会导致vector容器的所有迭代器都失效。
     我们看到实现上述所说的分配和撤销内存空间的方式以实现vector的自增长性,效率是极其低下的。为了使vector容器实现快速的内存分配,实际分配的容器会比当前所需的空间多一些,vector容器预留了这些额外的存储区,用来存放新添加的元素,而不需要每次都重新分配新的存储空间。你可以从vector里实现capacity和reserve成员可以看出这种机制。
    capacity和size的区别:size是容器当前拥有的元素个数,而capacity则指容器在必须分配新存储空间之前可以存储的元素总数。
    vector迭代器的几种失效的情况: 
1、当插入(push_back)一个元素后,end操作返回的迭代器肯定失效。
2、当插入(push_back)一个元素后,capacity返回值与没有插入元素之前相比若有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器都会失效。
3、当进行删除操作(erase,pop_back)后,指向删除点的迭代器全部失效;指向删除点后面的元素的迭代器也将全部失效。
 

对于序列式容器(如vector,deque),序列式容器就是数组式容器,删除当前的iterator会使后面所有元素的iterator都失效。这是因为vetor,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。所以不能使用erase(iter++)的方式,还好erase方法可以返回下一个有效的iterator。

for (iter = cont.begin(); iter != cont.end();)
{
   (*it)->doSomething();
   if (shouldDelete(*iter))
      iter = cont.erase(iter);  //erase删除元素,返回下一个迭代器
   else
      ++iter;
}

迭代器失效:

void vectorTest()
{
    vector<int> container;
    for (int i = 0; i < 10; i++)
        container.push_back(i);

    vector<int>::iterator iter;
     for (iter = container.begin(); iter != container.end(); iter++)
    {
            if (*iter > 3)
              container.erase(iter);
    }

     for (iter = container.begin(); iter != container.end(); iter++)
    {
            cout<<*iter<<endl;
    }
}

迭代器在执行++操作时报错!已经失效的迭代器不能再进行自增运算了。

       对于序列式容器,比如vector,删除当前的iterator会使后面所有元素的iterator都失效。这是因为顺序容器内存是连续分配(分配一个数组作为内存),删除一个元素导致后面所有的元素会向前移动一个位置。(删除了一个元素,该元素后面的所有元素都要挪位置,所以,iter++,已经指向的是未知内存)。

但是erase方法可以返回下一个有效的iterator。所以代码做如下修改,就OK了。

void vectorTest()
{
    vector<int> container;
    for (int i = 0; i < 10; i++)
        container.push_back(i);

    vector<int>::iterator iter;
     for (iter = container.begin(); iter != container.end();)
    {
            if (*iter > 3) 
		iter = container.erase(iter);
	    else 
		iter ++;
    }

     for (iter = container.begin(); iter != container.end(); iter++)
            cout<<*iter<<endl;
}

总结:vector是一个顺序容器,在内存中是一块连续的内存,当删除一个元素后,内存中的数据会发生移动,以保证数据的紧凑。所以删除一个数据后,其他数据的地址发生了变化,之前获取的迭代器根据原有的信息就访问不到正确的数据。

所以为了防止vector迭代器失效,常用如下方法:

for (iter = container.begin(); iter != container.end(); )
{
            if (*iter > 3)
              iter = container.erase(iter);    //erase的返回值是删除元素下一个元素的迭代器
            else{
                iter++;
            }
 }

这样删除后iter指向的元素后,返回的是下一个元素的迭代器,这个迭代器是vector内存调整过后新的有效的迭代器。

1. 对于关联容器(如map, set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前iterator即可。这是因为map之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响。erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。

for (iter = cont.begin(); it != cont.end();)
{
   (*iter)->doSomething();
   if (shouldDelete(*iter))
      cont.erase(iter++);
   else
      ++iter;
}

void mapTest()
{
    map<int, string> dataMap;

    for (int i = 0; i < 100; i++)
    {
           string strValue = "Hello, World";

            stringstream ss;
            ss<<i;
            string tmpStrCount;
            ss>>tmpStrCount;
            strValue += tmpStrCount;
            dataMap.insert(make_pair(i, strValue));
    }

    cout<<"MAP元素内容为:"<<endl;
     map<int, string>::iterator iter;
    for (iter = dataMap.begin(); iter != dataMap.end(); iter++)
    {
            int nKey = iter->first;
            string strValue = iter->second;
            cout<<strValue<<endl;
    }

    cout<<"内容开始删除:"<<endl;
    /////////////////////////////////////////////擦除操作引发迭代器失效
    for (iter = dataMap.begin(); iter != dataMap.end();iter++)
    {
            int nKey = iter->first;
            string strValue = iter->second;

           if (nKey % 2 == 0)
                dataMap.erase(iter);//出错

           /* cout<<iter->second<<endl;*/
    }
}

解析:dataMap.erase(iter)之后,iter就已经失效了,所以iter无法自增,即iter++就会出bug.解决方案,就是在iter失效之前,先自增。

for (iter = dataMap.begin(); iter != dataMap.end();)  
{  
            int nKey = iter->first;  
            string strValue = iter->second;  
  
           if (nKey % 2 == 0)  
           {  
                dataMap.erase(iter++);  
                auto a = iter;  
           }  
           else  
               iter ++;   
} 

解析:dataMap.erase(iter++);这句话分三步走,先把iter传值到erase里面,然后iter自增,然后执行erase,所以iter在失效前已经自增了。

map是关联容器,以红黑树或者平衡二叉树组织数据,虽然删除了一个元素,整棵树也会调整,以符合红黑树或者二叉树的规范,但是单个节点在内存中的地址没有变化,变化的是各节点之间的指向关系。

所以在map中为了防止迭代器失效,在有删除操作时,常用如下方法:

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++;
   }

 map<int, string>::iterator tmpIter =iter; iter++;

 dataMap.erase(tmpIter);

这几句的意思是,先保留要删除的节点迭代器,再让iter向下一个有意义的节点,然后删除节点。

所以这个操作结束后iter指向的是下一个有意义的节点,没有失效。

其实这三句话可以用在一句话代替,就是dataMap.erase(iter++);

解释是先让iter指向下一个有效的节点,但是返回给erase函数的是原来的iter副本。这个可能跟++这个操作的本身语法相关。

但是功能跟上面是一样的。


 deque迭代器的失效情况: 在C++Primer一书中是这样限定的: 
1、在deque容器首部或者尾部插入元素不会使得任何迭代器失效。 
2、在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。
3、在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器失效。

 

总结:迭代器失效分三种情况考虑,也是非三种数据结构考虑,分别为数组型vector,链表型list,树型数据结构(map set multimap multiset)。

数组型数据结构:该数据结构的元素是分配在连续的内存中,插入push_back和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除点之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后再iter++,是没有意义的。解决方法:erase(*iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter);

链表型数据结构:对于list型的数据结构,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是其他迭代器不会失效.解决办法两种,erase(*iter)会返回下一个有效迭代器的值,或者erase(iter++).

树形数据结构: 使用红黑树来存储数据,插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是其他迭代不会失效.erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。

注意:经过erase(iter)之后的迭代器完全失效,该迭代器iter不能参与任何运算,包括iter++,*iter


### STL `erase` 方法导致迭代器失效的问题 当使用 C++ 标准模板库 (STL) 的容器成员函数 `erase` 删除元素时,可能会使某些迭代器失效。这取决于所使用的具体容器类型。 对于 **序列式容器** 如 `std::vector`, 当执行 `erase` 操作移除某个位置上的元素之后,在该位置及其后的所有迭代器都会被视作已失效[^5]。这意味着如果尝试继续使用这些旧有的迭代器来访问或修改容器内的其他项,则可能导致未定义行为。 而对于像 `std::list` 或者双向链表这样的双端开口列表来说,只有指向被删节点本身的那个特定迭代器会被标记为无效;其余部分保持有效状态不变。 至于 **关联式容器**, 例如 `std::map` 和 `std::set`, 它们内部实现通常基于平衡树结构。因此在这类集合上调用 `erase` 函数仅会使对应键值对映射关系断开连接,并不会影响到任何现存的有效迭代器指针。 #### 解决方案 为了安全地遍历并可能从中删除项目而不引起迭代器失效问题: - 使用返回新合法迭代器的方法:许多容器提供了一个版本的 `erase` ,它接受单个参数(即要擦除的位置),并且返回紧跟其后的下一个可用位置的一个迭代器。这样可以在循环体内直接更新当前迭代器变量。 ```cpp for(auto it = vec.begin(); it != vec.end();) { if(/* condition */) { it = vec.erase(it); } else { ++it; } } ``` - 利用范围形式的 `erase`: 如果可以预先知道哪些区间内全部都需要清除掉的话,那么可以直接指定起始结束边界来进行批量处理操作,从而减少不必要的多次单独调用所带来的性能损耗以及潜在风险。 ```cpp auto start_it = find_if(vec.begin(), vec.end(), /* predicate */); auto end_it = remove_if(start_it, vec.end(), /* another_predicate */); vec.erase(start_it, end_it); ``` - 针对支持随机存取特性的容器如数组向量等特殊情形下还可以考虑采用先交换再弹出的方式完成目标对象清理工作,此做法能够有效地规避因连续内存布局特性引发的一系列连锁反应式的迭代器破坏现象发生。 ```cpp if (!vec.empty() && should_remove_last_element()) { std::swap(*find_position_to_erase, vec.back()); vec.pop_back(); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值