STL序列式容器中删除元素的方法和陷阱(一)

 在STL(标准模板库)中经常会碰到要删除容器中部分元素的情况,本人在编程中就经常编写这方面的代码,在编码和测试过程中发现在STL中删除容器有很多陷阱,网上也有不少网友提到如何在STL中安全删除元素这些问题。本文将讨论编程过程中最经常使用的两个序列式容器vector、list中安全删除元素的方法和应该注意的问题, 其它如queue、stack等配接器容器(container adapter),由于它们有专属的操作行为,没有迭代器(iterator),不能采用本文介绍的删除方法,至于deque,它与vector的删除方法一样。STL容器功能强大,but no siliver bullet,如果你使用不当,也将让你吃尽苦头。
  1.手工编写for循环代码删除STL序列式容器中元素的方法
  例如,你能看出以下代码有什么问题?
  例1:
  #include <iostream>
  #include <vector>
  using namespace std;
  void main( ) {
   vector<int> vectInt;
   int i;
   // 初始化vector容器
   for (i = 0; i < 5; i++ ) {
   vectInt.push_back( i );
   }
   // 以下代码是要删除所有值为4的元素
   vector<int>::iterator itVect = vectInt.begin();
   for ( ; itVect != vectInt.end(); ++itVect ) {
   if ( *itVect == 4 ) {
   vectInt.erase( itVect );
   }
   }
   int iSize = vectInt.size();
   for ( i = 0 ; i < iSize; i++ ) {
   cout << " i= " << i << ", " << vectInt[ i ] << endl;
   }
  }
  例2:
  #include <iostream>
  #include <vector>
  using namespace std;
  void main( ) {
   vector<int> vectInt;
   int i;
   // 初始化vector容器
   for ( i = 0; i < 5; i++ ) {
   vectInt.push_back( i );
   if ( 3 == i ) {
   // 使3的元素有两个,并且相临。这非常关键,否则将发现不了bug。
   // 具体解释见下。
   vectInt.push_back( i );
   }
   }
   vector<int>::iterator itVect = vectInt.begin();
   vector<int>::iterator itVectEnd = vectInt.end(); // 防止for多重计算
   // 以下代码是要删除所有值为3的元素
   for ( ; itVect != itVectEnd; ++itVect ) {
   if ( *itVect == 3 ) {
   itVect = vectInt.erase( itVect );
   }
   }
   int iSize = vectInt.size();
   for ( i = 0 ; i < iSize; i++ ) {
   cout << " i= " << i << ", " << vectInt[ i ] << endl;
   }
  例3:
  #include <iostream>
  #include <vector>
  using namespace std;
  void main( ) {
   vector<int> vectInt( 5 );
   int i;
   vectInt[ 0 ] = 0;
   vectInt[ 1 ] = 1;
   vectInt[ 2 ] = 2;
   vectInt[ 3 ] = 3;
   vectInt[ 4 ] = 4; // 替换为 vectInt[ 4 ] = 3;试试
   vector<int>::iterator itVect = vectInt.begin();
   vector<int>::iterator itVectEnd = vectInt.end(); // 防止for多重计算
   // 以下代码是要删除所有值为3的元素
   for ( ; itVect != itVectEnd; ) {
   if ( *itVect == 3 ) {
   itVect = vectInt.erase( itVect );
   }
   else {
   ++itVect;
   }
   }
   int iSize = vectInt.size();
   for ( i = 0 ; i < iSize; i++ ) {
   cout << " i= " << i << ", " << vectInt[ i ] << endl;
   }
  }
  分析:
  这里最重要的是要理解erase成员函数,它删除了itVect迭代器指向的元素,并且返回要被删除的itVect之后的迭代器,迭代器相当于一个智能指针,指向容器中的元素,现在删除了这个元素,将导致内存重新分配,相应指向这个元素的迭代器之后的迭代器就失效了,但erase成员函数返回要被删除的itVect之后的迭代器。
  例1将导致程序未定义的错误,在windows中即是访问非法内存,程序当掉。因为vectInt.erase( itVect );调用后itVect之后的迭代器已无效了,所以当执行++itVect后,*itVect访问了非法内存。例1也是初学者最容易犯的错误,这个错误也比较容易发现。
  例2可能会导致不能把vectInt中所有为3的元素删除掉。因为第一次删除成功时,itVect = vectInt.erase( itVect );itVect为指向3之后的位置,之后再执行++itVect,itVect就掉过了被删除元素3之后的元素3,导致只删除了一个为3的元素,这个bug比较隐蔽,因为如果不是两个均为3的元素相临,就将很难捕捉到这个bug,程序有可能在一段时间运行良好,但如碰到容器中两值相同的元素相临,则程序就要出问题。
  例3,对于本例你可能要说程序没有任何问题,解决了上面的两个bug,程序也运行正常。但且慢,你把 “vectInt[ 4 ] = 4;” 这一行改为 “vectInt[ 4 ] = 3;”试试,一运行,程序当掉,访问非法内存!你疑惑不解:从程序看不出bug,而且我还把vectInt.end()放在外面计算以防止for多重计算,提高效率。哈哈,问题就出在最后一句话!算法大师Donald Knuth有一句名言:不成熟的优化是一切恶果的根源( Permature optimization is the root of all evil )。由于在for循环中要删除元素,则vectInt.end()是会变化的,所以不能在for循环外计算,而是每删除一次都要重新计算,所以应放在for循环内。那你要问,为什么把 “vectInt[ 4 ] = 4;” 这一行改为 “vectInt[ 4 ] = 3;”程序就会当掉,而不改程序就很正常呢?这就跟vector的实现机制有关了。下面以图例详细解释。
  vectInt的初始状态为:
   | end
  0 1 2 3 4
  
  删除3后,
   |新的end | 原来的end
  0 1 2 4 4
  
  注意上面“新的end”指向的内存并没有被清除,为了效率,vector会申请超过需要的内存保存数据,删除数据时也不会把多余的内存删除。
  然后itVect再执行++itVect,因为此时*itVect等于4,所以继续循环, 这时itVect 等于“新的end”但不等于“原来的end”(它即为itVectEnd),所以继续,因为 *itVect访问的是只读内存得到的值为4,不等于3,故不删除,然后执行++itVect此时itVect等于itVectEnd退出循环。从上面过程可以看出,程序多循环了一次(删除几次,就要多循环几次),但程序正常运行。
  如果把 “vectInt[ 4 ] = 4;” 这一行改为 “vectInt[ 4 ] = 3;”过程如下:
  
   | end
  0 1 2 3 3
  删除3后,
   |新的end |原来的 end
  0 1 2 3 3
  
  删除第2个3后,
   |新的end |原来的 end
  0 1 2 3 3
  这时itVect 等于“新的end”但不等于“原来的end”(它即为itVectEnd),所以继续,因为 *itVect访问的是只读内存得到的值为3,等于3,所以执行删除,但因为*itVect访问的是只读内存不能删除,所以程序当掉。
  综上,我们知道当要删除的值在容器末尾时,会导致程序删除非法内存,程序当掉;即使程序正常运行,也是for循环多执行了等于删除个数的循环。所以把vectInt.end()放在for循环外面执行,完全是错误的。对于list容器,list.end()在删除过程中是不会变的,可以把它放在for循环外面计算,但由于list.end()是个常量,把list.end()放在for循环中计算编译器应该可以优化它。从安全考虑,除非你能保证for循环中不会改变容器的大小,否则都应该对容器的值在for循环中计算,对于 vectInt.size()这样的计算,也应该在for循环中计算,不要因为微小的优化而导致程序出错。
  正确的方法:
  例4:
  #include <iostream>
  #include <vector>
  using namespace std;
  void main( ) {
   vector<int> vectInt;
   int i;
   for ( i = 0; i < 5; i++ ) {
   vectInt.push_back( i );
   if ( 3 == i ) {
   // 使3的元素有两个,并且相临。
   vectInt.push_back( i );
   }
   }
   vector<int>::iterator itVect = vectInt.begin();
   // 以下代码是要删除所有值为3的元素
   for ( ; itVect != vectInt.end(); ) { // 删除 ++itVect{
   if ( *itVect == 3 ) {
   itVect = vectInt.erase( itVect );
   }
   else {
   ++itVect;
   }
   }
   // 把vectInt.size()放在for循环中
   for ( i = 0 ; i < vectInt.size(); i++ ) {
   cout << " i= " << i << ", " << vectInt[ i ] << endl;
   }
  运行结果为:
  i= 0, 0
  i= 1, 1
  i= 2, 2
  i= 3, 4
  从结果显示值为3的元素确实被删除了。

 

2.使用STL中通用算法或容器成员函数删除元素的方法
  以上手工编写for循环代码删除容器中元素的方法也有一些问题,如果判断条件特别复杂,又有循环判断的话,循环中间又有异常处理的话,++itVect的位置就要小心放置了,稍不留意就要出错。所以手工编写代码删除容器中元素的方法不太安全,代码重复,也不够优雅,要注意的地方很多。
  对于这种情况,可以考虑使用STL中通用算法remvoe()和remove_if()帮忙。而remvoe()和remove_if()这两个算法也有一个问题需要程序员特别小心。在通用算法中的 remove(包括remove_if) 函数,并不真正从容器中删除元素,而是“应被删除的元素”被其后的“未被删除的元素”覆盖。返回值ForwardIterator指向经移除后的最后元素的下一位置。如vector{0,1,2,3,3,4},执行remove(),希望移除所有值为3的元素,结果为{0,1,2,4,3,4},返回值ForwardIterator指向第5个元素。即:
  0 1 2 3 3 4 移除前
  0 1 2 4 3 4 移除后
  移除值为3的元素。移除后3被其后的4替代,最后两位元素为残余数据。
  例 5:
  void main() {
   vector<int> vectInt;
   int i;
   for (i = 0; i < 5; i++ ) {
   vectInt.push_back( i );
   if ( 3 == i ) {
   vectInt.push_back( i );
   }
   }
   remove( vectInt.begin(), vectInt.end(), 3 );
   cout << " after deleted , size = " << vectInt.size() << endl;
   for ( i = 0; i < vectInt.size();; i++ ) {
   cout << "i = " << i << " , " << vectInt[i] << endl;
   }
  }
  运行结果为:
  after deleted , size = 6 // 从这行可以看出,移除后容器的大小没变
  i = 0 , 0
  i = 1 , 1
  i = 2 , 2
  i = 3 , 4 // 从这行可以看出:“应被删除的元素”3 被其后的“未被删除的元素”4覆盖
  i = 4 , 3
  i = 5 , 4
  所以要彻底删除还应该把后面的残余数据删除掉,这可以通过调用容器的成员函数erase()做到。
  例 6:
  void main() {
   vector<int> vectInt;
   int i;
   for (i = 0; i < 5; i++ ) {
   vectInt.push_back( i );
   if ( 3 == i ) {
   vectInt.push_back( i );
   }
   }
   vectInt.erase( remove( vectInt.begin(), vectInt.end(), 3 ), vectInt.end() );
   cout << " after deleted , size = " << vectInt.size() << endl;
   for ( i = 0; i < vectInt.size();; i++ ) {
   cout << "i = " << i << " , " << vectInt[i] << endl;
   }
  }
  运行结果为:
  after deleted , size = 4 // 从这行可以看出,删除后容器的大小变化了
  i = 0 , 0
  i = 1 , 1
  i = 2 , 2
  i = 3 , 4
  从结果可以看出,所有值为3的元素确实被删除了。
  对于vector容器存放其他比较复杂的对象,就可以用remove_if()加函数对象(Function Object)的方法。
  如:
  例7:
  #include <iostream>
  #include <sstream>
  #include <string>
  #include <vector>
  #include <algorithm>
  #include <list>
  using namespace std;
  class CTest {
  public:
   CTest( const string& str, int iPrice ) : m_strName( str ), m_iPrice( iPrice ) { }
   void vPrint() { cout << "name=" << m_strName << " price = " << m_iPrice << endl;
   }
  private:
   string m_strName;
   int m_iPrice;
   // 由于两个函数对象要访问CTest类的private成员,所以设为友员。
   friend class CStrFunc;
   friend class CIntFunc;
  };
  // 函数对象,根据string比较
  class CStrFunc {
   string m_str;
  public:
   CStrFunc( const string& str ) : m_str( str ) {
   }
   bool operator() ( const CTest& left ) {
   return ( m_str == left.m_strName ) ? true : false;
   }
  };
  // 函数对象,根据int比较
  class CIntFunc {
   int m_iPrice;
  public:
   CIntFunc( int iPrice ) : m_iPrice( iPrice ) {
   }
   bool operator() ( const CTest& left ) {
   return ( m_iPrice == left.m_iPrice ) ? true : false;
   }
  };
  void main( ) {
   vector< CTest > vectTest;
   int i;
   for ( i = 0; i < 5 ; i++ ) {
   stringstream stream; // 流格式化符,把int转化为string
   stream << i;
   string str = stream.str();
   CTest clTest( str, i );
   vectTest.push_back( clTest );
   }
   for ( i = 0 ; i < vectTest.size(); i++ ) {
   vectTest[ i ].vPrint();
   }
   // 删除所有m_strName = "3"的元素
   vectTest.erase( remove_if( vectTest.begin(), vectTest.end(), CStrFunc( "3" ) ),
   vectTest.end() );
   cout << "delete 3 after : " << endl;
   for ( i = 0 ; i < vectTest.size(); i++ ) {
   vectTest[ i ].vPrint();
   }
   // 删除所有m_iPrice = 2的元素
   vectTest.erase( remove_if( vectTest.begin(), vectTest.end(), CIntFunc( 2 ) ),
   vectTest.end() );
   cout << "delete 2 after : " << endl;
   for ( i = 0 ; i < vectTest.size(); i++ ) {
   vectTest[ i ].vPrint();
   }
  }
  手工编写for循环代码删除STL序列式容器中元素的方法,使用STL中通用算法或容器成员函数删除元素的方法,两者之间的比较:
  1. 前者代码重复。
  2. 前者容易出错,不够清晰。
  3. 效率:
  0 1 2 3 2 5 6 7
  0 1 3 2 5 6 7
  0 1 3 5 6 7
  用第一种方法删除所有值为2的元素
  从上图可以看出,每删除一个元素,后面的所有元素都到往前移动一位,导致一次内存大搬迁。
  0 1 2 3 2 5 6 7
  0 1 3 2 5 6 6 7
  0 1 3 5 6 7
  用第二种方法删除所有值为2的元素
  从上面可以看出,删除时元素2被后面元素覆盖,不会到元素移位和内存大搬迁,残余数据留到末尾一次全部删除,也不会导致内存大搬迁,所以后者的方法要比前者在效率上好很多。

 

出处:http://www.wangchao.net.cn/bbsdetail_27824.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值