list、vector使用erase()時需要注意的地方——迭代器失效
先說一下兩者的優缺點吧。
list與vector的區別
vector相當於一個數組。
在記憶體中分配一塊連續的記憶體空間進行儲存。支援不指定vector大小的儲存。STL內部實現時,首先分配一個非常大的記憶體空間預備進行儲存,即capacituy()函式返回的大小,當超過此分配的空間時再整體重新放分配一塊記憶體儲存,這給人以vector可以不指定vector即一個連續記憶體的大小的感覺。通常此預設的記憶體分配能完成大部分情況下的儲存。
優點:(1) 不指定一塊記憶體大小的陣列的連續儲存,即可以像陣列一樣操作,但可以對此陣列
進行動態操作。通常體現在push_back() pop_back()
(2) 隨機訪問方便,即支援[ ]操作符和vector.at()
(3) 節省空間。
缺點:(1) 在內部進行插入刪除操作效率低。
(2) 只能在
(3) 當動態新增的資料超過vector預設分配的大小時要進行整體的重新分配、拷貝與釋放 。
list(雙向連結串列)
每一個結點都包括一個資訊快Info、一個前驅指標Pre、一個後驅指標Post。可以不分配必須的記憶體大小方便的進行新增和刪除操作。使用的是非連續的記憶體空間
優點:(1) 不使用連續記憶體完成動態操作。
(2) 在內部方便的進行插入和刪除操作
(3) 可在兩端進行push、pop
缺點:(1) 不能進行內部的隨機訪問,即不支援[ ]操作符和vector.at()
(2) 相對於verctor佔用記憶體多。
問題情景
分別使用vector、list容器實現對1, 4, 3, 7, 9, 3, 6, 8, 3, 3, 5, 2, 3, 7的存入,並且刪除其中所有的3,最後輸出顯示剩下的數字。
面對這個問題,喵哥首先想到的是用erase刪除,那麼在list和vector中使用erase一樣麼?
vector使用erase
vector使用erase刪除元素,其返回值指向下一個元素,但是由於vector本身的性質(存在一塊連續的記憶體上),刪掉一個元素後,其後的元素都會向前移動,所以此時指向下一個元素的迭代器其實跟剛剛被刪除元素的迭代器是一樣的。
圖中的1001、1002~……表示記憶體地址的關係。
一下是喵哥的解決方案:
#include <vector>
#include <iostream>
using namespace std;
int main()
{
int a[] = {1, 4, 3, 7, 9, 3, 6, 8, 3, 3, 5, 2, 3, 7};
vector<int> vector_int(a, a + sizeof(a)/sizeof(int));
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*方案一*/
// for(int i = 0; i < vector_int.size(); i++)
// {
// if(vector_int[i] == 3)
// {
// vector_int.erase(vector_int.begin() + i);
// i--;
// }
// }
/*方案二*/
// for(vector<int>::iterator itor = vector_int.begin(); itor != vector_int.end(); ++itor)
// {
// if (*itor == 3)
// {
// vector_int.erase(itor);
// --itor;
// }
// }
/*方案三*/
vector<int>::iterator v = vector_int.begin();
while(v != vector_int.end())
{
if(*v == 3)
{
v = vector_int.erase(v);
cout << *v << endl;
}
else{
v++;
}
}
/*方案四*/
// vector<int>::iterator v = vector_int.begin();
// while(v != vector_int.end())
// {
// if(*v == 3)
// {
// vector_int.erase(v);
// }
// else{
// v++;
// }
// }
for(vector<int>::iterator itor = vector_int.begin(); itor != vector_int.end(); itor++)
{
cout << * itor << " ";
}
cout << endl;
return 0;
}
一共有四種方案。
方案一表明vector可以用下標訪問元素,顯示出其隨機訪問的強大。並且由於vector的連續性,且for迴圈中有迭代器的自加,所以在刪除一個元素後,迭代器需要減1。
方案二與方案一在迭代器的處理上是類似的,不過對元素的訪問採用了迭代器的方法。
方案三與方案四基本一致,只是方案三利用了erase()函式的返回值是指向下一個元素的性質,又由於vector的性質(連續的記憶體塊),所以方案四在erase後並不需要對迭代器做加法。
list使用erase
list的空間不是連續的,所以刪除一個元素後,其餘的元素不會發生變化,那麼在刪除一個元素後就需要對迭代器做加法使其指向下一個元素,或者利用erase的返回值(指向下一個元素的迭代器)。
圖中的1001、2002……表示記憶體地址的關係。
#include <list>
#include <iostream>
using namespace std;
int main()
{
int a[] = {1, 4, 3, 7, 9, 3, 6, 8, 3, 3, 5, 2, 3, 7};
list<int> list_int(a, a + sizeof(a)/sizeof(int));
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
方法一
*/
// for(list<int>::iterator itor = list_int.begin(); itor != list_int.end(); ++itor)
// {
// if (*itor == 3)
// {
// list_int.erase(itor);
// itor--;
// }
// }
/*
方法二
*/
// list<int>:: iterator l = list_int.begin();
// while(l != list_int.end())
// {
// if(*l == 3)
// {
// l = list_int.erase(l);
// }
// else
// {
// l++;
// }
// }
/*
方法三
*/
list<int>:: iterator l = list_int.begin();
while(l != list_int.end())
{
if(*l == 3)
{
list_int.erase(l++);
// cout << *l << endl;
}
else
{
l++;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(list<int>::iterator itor = list_int.begin(); itor != list_int.end(); itor++)
{
cout << * itor << " ";
}
cout << endl;
return 0;
}
一共三個方案,其中的方案二跟方案三大致一樣,只是方案二利用了erase的返回值是指向下一個元素的迭代器的性質。
值得注意的是,方案一中在刪除一個元素後還對迭代器做了減法,原因是list執行erase後,被刪除元素的迭代器已經失效,無法再做自加,程式會崩潰。所以先做自減(暫時不知道為什麼只能做自減,不能做自加,猜測list在使用erase後沒有刪掉前驅指標?)。
另外,方案三的list_int.erase(l++);不可以分解為list_int.erase(l);和l++;,這樣程式會崩潰,因為l++其返回值是l,並且是完成l自加後才執行erase的。
最後推薦一個在Ubuntu下超好用的繪圖軟體Pinta,功能強大,有圖層編輯。
//2018年11月28日10:51:03
回答一下上面自己的疑問。
list在執行刪除後,其餘的迭代器有效,但是被刪除的迭代器失效,不可以再對其做自加自減的,一上程式碼我是在Ubuntu的gcc4.8.4編譯的,但是在Windows的VS2015中執行,程式就會崩潰。
同理,在vector中也不可以這樣使用(方法二)。
可以考慮把itor的自減放到erase裡面,即
.erase(itor--);
這樣就在執行刪除迭代器前,itor就自減了,不會發生崩潰。並且後置的自減可以做到返回值還是自減前的取值。