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

本文轉自 http://blog.csdn.net/canco/

 dd在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的初始狀態爲:
                                                                          

0        1        2        3         4       | end


刪除3後,


0        1        2        4         4       |新的end  | 原來的end

 

注意上面“新的end”指向的內存並沒有被清除,爲了效率,vector會申請超過需要的內存保存數據,刪除數據時也不會把多餘的內存刪除。

然後itVect再執行++itVect,因爲此時*itVect等於4,所以繼續循環, 這時itVect 等於“新的end”但不等於“原來的end”(它即爲itVectEnd),所以繼續,因爲 *itVect訪問的是隻讀內存得到的值爲4,不等於3,故不刪除,然後執行++itVect此時itVect等於itVectEnd退出循環。從上面過程可以看出,程序多循環了一次(刪除幾次,就要多循環幾次),但程序正常運行。

如果把 “vectInt[ 4 ] = 4;” 這一行改爲 “vectInt[ 4 ] = 3;”過程如下:

 0        1        2        3         3      | end

刪除3後,

 0        1        2        3         3      |新的end       |原來的 end

 刪除第2個3後,

0        1        2        3         3      |新的end            |原來的 end

 這時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的元素確實被刪除了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章