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

gugu99發表於2008-05-26
STL序列式容器中刪除元素的方法和陷阱 一 (轉)[@more@]

在STL(標準模板庫)中經常會碰到要刪除容器中部分元素的情況,本人在中就經常編寫這方面的程式碼,在編碼和測試過程中發現在STL中刪除容器有很多陷阱,網上也有不少網友提到如何在STL中刪除元素這些問題。本文將討論程式設計過程中最經常使用的兩個序列式容器vector、list中安全刪除元素的方法和應該注意的問題,  其它如queue、stack等配接器容器(container adapter),由於它們有專屬的操作行為,沒有迭代器(iterator),不能採用本文介紹的刪除方法,至於deque,它與vector的刪除方法一樣。STL容器功能強大,but no siliver bullet,如果你使用不當,也將讓你吃盡苦頭。

1.手工編寫for迴圈程式碼刪除STL序列式容器中元素的方法:namespace prefix = o ns = "urn:schemas--com::office" />

例如,你能看出以下程式碼有什麼問題?

例1:

#include

#include

using namespace std;

void main( ) {

  vector vectInt;

  int i;

  //  初始化vector容器

  for (i = 0; i < 5; i++ ) {

    vectInt.push_back( i );

  }

  //  以下程式碼是要刪除所有值為4的元素

  vector::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 <

  }

 

}

例2:

#include

#include

using namespace std;

void main( ) {

  vector vectInt;

  int i;

  //  初始化vector容器

  for ( i = 0; i < 5; i++ ) {

    vectInt.push_back( i );

  if ( 3 == i ) {

  //  使3的元素有兩個,並且相臨。這非常關鍵,否則將發現不了。

  //  具體解釋見下。

    vectInt.push_back( i );

  }

  }

  vector::iterator itVect = vectInt.begin();

  vector::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 <

  }

例3:

#include

#include

using namespace std;

void main( ) {

  vector vectInt( 5 );

  int i;

  vectInt[ 0 ] = 0;

  vectInt[ 1 ] = 1;

  vectInt[ 2 ] = 2;

  vectInt[ 3 ] = 3;

  vectInt[ 4 ] = 4; //  替換為 vectInt[ 4 ] = 3;試試

  vector::iterator itVect = vectInt.begin();

  vector::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 <

  }

}

 

分析:

這裡最重要的是要理解erase成員,它刪除了itVect迭代器指向的元素,Mailto:e4e dateTime=2002-10-28T12:47>並且返回要被刪除的itVect之後的迭代器,迭代器相當於一個智慧指標,指向容器中的元素,現在刪除了這個元素,將導致重新分配,相應指向這個元素的迭代器之後的迭代器就失效了,但erase成員函式返回要被刪除的itVect之後的迭代器

例1將導致未定義的錯誤,在中即是訪問記憶體,程式當掉。因為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 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

#include

using namespace std;

void main( ) {

  vector vectInt;

  int i;

  for (  i = 0; i < 5; i++ ) {

    vectInt.push_back( i );

  if ( 3 == i ) {

  //  使3的元素有兩個,並且相臨。

    vectInt.push_back( i );

  }

  }

  vector::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 <

  }

執行結果為:

i= 0, 0

i= 1, 1

i= 2, 2

i= 3, 4

從結果顯示值為3的元素確實被刪除了。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-1004615/,如需轉載,請註明出處,否則將追究法律責任。

相關文章