迭代器失效-小心使用STL容器的erase()

对于以下代码:

 

[cpp] view plaincopy
 
  1.    my_container.erase(iter);  

                其中my_container是STL的某种容器,iter是指向这个容器中某个元素的迭代器。如果不是在for,while循环中,

        这种方式删除元素没有问题,如果是在for,while中对m_container迭代,删除其中符合条件的所有元素,就可能出现问题。

 

         如果是在for,while中对m_container迭代,删除其中符合条件的所有元素,就可能出现问题。

 

        问题是:

                在迭代容器的时候删除元素,可能导致迭代器失效(invalidation of iterators),产生未定义行为

         (undefined behavior);

                例如,对某个迭代器解引用所获得的值并不是执行erase()前这个迭代器指向的值,还有可能对未指向任何

         元素的迭代器的解引用赋值而引发程序crash。

         类似的问题代码像这样:

 

[cpp] view plaincopy
 
  1. std::vector<int>  my_container;  
  2. for (int i = 0; i < 100; ++i) {  
  3.      my_container.push_back(i);  
  4. bsp;   }  
  5.   
  6. std::vector<int>::iterator it = my_container.begin();  
  7. for (it != my_container.end(); it++) {  
  8.     if (*it % 2 == 1) {  
  9.          my_container.erase(it);  
  10.      }  
  11. }  

                my_container.erase(it)之后,it及其后面的迭代器已经失效,不应该再使用这些迭代器。再执行it++,其行为是未定义的。

 

       其他容器也会遇到迭代器失效的问题:

                对于vector 被删除元素的迭代器以及指向后面元素的迭代器全部失效。  

                对于deque  在首部或尾部删除元素则只会使指向被删除元素的迭代器失效,任何其他位置的插入和删除操作将使指向该容器元素的

          所有迭代器失效。  

                对于list 仅有指向被删除元素的迭代器失效。    

                对于(mulit)map ,(multi)set 仅有指向被删除元素的迭代器失效。

 

              所以Golden Rule是:尽量不要使用容器的插入删除操作之前的迭代器。

              为什么不同容器迭代器失效情况有差别?这与实现各容器的数据结构有关。  

              如何在迭代容器时删除其中的元素?各容器通用的做法如下:

[cpp] view plaincopy
 
  1.          std::vector<int>::iterator it = my_container.begin();  
  2.          for (it != my_container.end();/**blank*/ ) {  
  3.               if (*it % 2 == 1) {  
  4.                    my_container.erase(it++);  
  5.               }  
  6.               else{  
  7.                    it++;  
  8.               }  
  9.          }  

 

                     my_container.erase(it++) 巧妙得在执行erase()之前,it 先自增,指向被删除元素后面的元素,而给erase()传递的是未自增的it迭代器,

            以定位要删除的元素。如果元素的值为奇数,则删除此元素,it指向下一个元素,如果元素的值为偶数,则检查下一个元素的值。整个迭代过程中

            迭代器就不会失效了。

                    上段代码中两个不同分支出现了i++操作,下面的代码示例显示了如何防止遗忘其中任何一个分支的i++操作。          

[cpp] view plaincopy
 
  1.             MyContainer::iterator it = myContainer.begin();  
  2.             While(it != myContainer.end()){  
  3.                MyContainer::iterator curIt = it;  
  4.                if (*curIt == matchingValue)    {  
  5.                        myContainer.erase(curIt);  
  6.                }  
  7.             }  

                    对于vector ,deque, list, 另一种可行的方式是:
                           

[cpp] view plaincopy
 
  1. std::vector<int>::iterator it = my_container.begin();  
  2. for (it != my_container.end();/**blank*/ ) {  
  3.    if (*it % 2 == 1) {  
  4.          it = my_container.erase(it);  
  5.      }  
  6.      else{  
  7.           it++;  
  8.      }  
  9. }  

                   上面代码可行的原因是vector::erase() 返回一个新的迭代器,指向被删除元素的后面的元素。可以继续使用新的迭代器。

 

                   而出于某种未知的原因(multi)map::erase(), (multi)set::erase()没有返回这样的迭代器。(从C++11开始也支持返回迭代器了).

                   

                   但是对于vector,诸如在0到99个数中删除所有奇数的问题,可以使用STL的remove(),remove_if()优化性能。代码如下:    

[cpp] view plaincopy
 
  1. bool isOdd(int value)  
  2. {  
  3.      return (value % 2) == 1;  
  4. }  
  5.   
  6. my_container.erase( std::remove_if(m_container.begin(), m_container.end(), isOdd), m_container.end());  

                   让我们再看看不使用remove_if()的版本:                 

[cpp] view plaincopy
 
  1. for (it != my_container.end();/**blank*/ ) {  
  2.    if (*it % 2 == 1) {  
  3.          it = my_container.erase(it);  
  4.      }  
  5.      else{  
  6.           it++;  
  7.      }  
  8.  }  

                   如果你阅读过erase()源码或了解它是如何工作的,性能问题就显而易见:erase()删除一个元素的操作是被删除元素后面的所有元素依次

           向前移动一个元素的位置,然后删除最后一个元素,时间复杂度为O(n^2)。

                   remove(),remove_if()的时间复杂度为O(n),删除元素的操作如下所示:

                 

[cpp] view plaincopy
 
  1. template<class ForwardIt, class UnaryPredicate>  
  2. ForwardIt remove_if(ForwardIt first, ForwardIt last, UnaryPredicate p)  
  3. {  
  4.       ForwardIt result = first;  
  5.       for (; first != last; ++first) {  
  6.            if (!p(*first)) {  
  7.                 *result++ = *first;  
  8.            }  
  9.       }  
  10.       return result;  
  11.  }  

                     从前向后遍历容器所有元素,将待保留的元素向前移动,占据待删除的元素的位置,remove_if()返回新的元素范围(begin,end)中的end,

 

             记为new_end_of_range ,再调用erase()删除从new_end_of_range到my_container.end()之间的所有元素。

                     实际上,remove_if() 没有删除容器中的任何元素,它没有改变my_container.end(), 调用remove_if()后容器元素个数不会改变!!删除元素的工作

              交给了erase().

                     

 

                       Scott Meyers在他的”Effective STL”中关于此问题的讨论中也使用了remove_if(),由此看来,他的确是提出了一些让STL effective的建议。

 

              深入学习STL迭代器失效问题:

                        在google中搜索 stl iterator invalidation rules 可以获得很多有关STL迭代器失效的有关内容。

            

              References:

              1. STL remove_if()       http://en.cppreference.com/w/cpp/algorithm/remove 

              2.More C++ Idioms/Erase-Remove   http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Erase-Remove

              3.Effective STL, Item 32 - Scott Meyers 

              4.Cpp Invalid Iterators [对各种迭代器失效的情况进行了讲解分类] 

                   http://www.angelikalanger.com/Conferences/Slides/CppInvalidIterators-DevConnections-2002.pdf                

              5.以下是stackoverflow上关于在迭代时删除容器中元素的讨论:

                  http://stackoverflow.com/questions/1604588/iterate-vector-remove-certain-items-as-i-go

                  http://stackoverflow.com/questions/3747691/stdvector-iterator-invalidation?rq=1

                  http://stackoverflow.com/questions/2874441/deleting-elements-from-stl-set-while-iterating?rq=1

                  http://stackoverflow.com/questions/1038708/erase-remove-contents-from-the-map-or-any-other-stl

                        -container-while-iterating/1038761#1038761

                  http://stackoverflow.com/questions/799314/difference-between-erase-and-remove?rq=1

 

其他总结:

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

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

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

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

 

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副本。这个可能跟++这个操作的本身语法相关。

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

posted @ 2015-01-13 16:38  jasononline  阅读(297)  评论(0编辑  收藏  举报