迭代器失效:99% 的 C++ 程序員都會踩的坑 !
嘿,各位 C++ 愛好者們,今天咱們聊一個幾乎所有 C++ 程序員都會踩的坑——迭代器失效。無論你是剛?cè)腴T的新手,還是寫了好幾年代碼的老司機,這個問題都可能讓你的程序莫名其妙地崩潰。不過別擔心,讀完這篇文章,你一定會恍然大悟:"哦!原來是這么回事!"
一、迭代器到底是個啥?
先別急著談"失效",咱們得先弄明白迭代器是啥玩意兒。
想象一下,迭代器就像是一個"指針",指向容器(比如vector、list)中的某個元素。通過迭代器,我們可以訪問、修改容器中的元素,還能在容器中移動(前進或后退)。
簡單來說,迭代器就是容器和算法之間的橋梁,讓你能夠不關心容器內(nèi)部結(jié)構,就能輕松遍歷和操作容器中的元素。
vector<int> nums = {1, 2, 3, 4, 5};
// it就是一個迭代器,指向vector的第一個元素
auto it = nums.begin();
cout << *it << endl; // 輸出1
二、什么是迭代器失效?
現(xiàn)在到了關鍵問題:什么是迭代器失效?
簡單講,當你對容器進行了某些操作后,原先有效的迭代器變得無效了,再使用這個迭代器就會導致未定義行為(通常是程序崩潰),這就是迭代器失效。
就好比你拿著一把鑰匙(迭代器)去開一個門(訪問容器元素),但有人趁你不注意把鎖換了(容器結(jié)構改變),你的鑰匙自然就不管用了。
三、常見的迭代器失效場景
1. vector中的迭代器失效
vector 是最常用的 STL 容器,也是迭代器失效最容易發(fā)生的地方。
場景一:添加元素(push_back)導致的失效
vector<int> nums = {1, 2, 3};
auto it = nums.begin(); // it指向1
nums.push_back(4); // 可能導致迭代器失效
cout << *it << endl; // 危險操作!可能崩潰
為啥會失效?因為 vector 在內(nèi)存中是連續(xù)存儲的,當空間不夠時,會重新分配一塊更大的內(nèi)存,并把原來的元素復制過去。這時候,原來的內(nèi)存地址就變了,之前的迭代器自然就失效了。
就像你正在看一本書,突然有人把這本書拿走換了一本新的放在原處——你手指的位置自然就不對了。
場景二:insert 操作導致的失效
說到 vector 添加元素,咱們可不能忘了另一個常用的操作——insert!這家伙比 push_back 還要狡猾呢!
vector<int> nums = {1, 2, 3, 4, 5};
auto it = nums.begin() + 2; // it指向元素3
nums.insert(nums.begin(), 0); // 在最前面插入0
cout << *it << endl; // 危險操作!it已經(jīng)失效了
為啥 insert 更容易讓人踩坑?因為 insert 有雙重殺傷力:
- 首先,和 push_back 一樣,如果 vector 容量不夠,insert會導致重新分配內(nèi)存,所有迭代器就全軍覆沒了。
- 其次,即使沒有重新分配內(nèi)存,insert也會讓插入位置及其后面的所有元素向后挪位置,這會使這些位置的迭代器全部"串位"。
打個比方,就像你排隊時,突然有人插隊到你前面,你和你后面的人都被迫向后移了一位——原來記錄的位置信息就全亂套了!
記住這個簡單規(guī)則:
- 如果 insert 導致擴容:所有迭代器都 GG
- 如果 insert 不導致擴容:插入位置及其后面的迭代器都 GG
場景三:刪除元素導致的失效
vector<int> nums = {1, 2, 3, 4, 5};
for (auto it = nums.begin(); it != nums.end(); ++it) {
if (*it == 3) {
nums.erase(it); // 迭代器失效
cout << *it << endl; // 不要繼續(xù)使用it,危險操作!可能崩潰
}
}
問題在哪?當你刪除了一個元素后,該位置后面的所有元素都會前移,原來的迭代器就指向了一個錯誤的位置。
2. list中的迭代器失效
list 是雙向鏈表,它的迭代器失效情況比 vector 要簡單些。
list<int> myList = {1, 2, 3, 4, 5};
auto it = myList.begin();
++it; // it指向2
myList.erase(it); // 刪除2,it失效
// 不能再使用it
對于 list,只有被刪除節(jié)點的迭代器會失效,其他節(jié)點的迭代器仍然有效。這是因為 list 是鏈表結(jié)構,刪除一個節(jié)點不會影響其他節(jié)點的內(nèi)存位置。
3. map/set中的迭代器失效
map 和 set 是基于紅黑樹實現(xiàn)的,它們的迭代器失效規(guī)則和 list 類似。
map<int, string> myMap = {{1, "one"}, {2, "two"}, {3, "three"}};
auto it = myMap.begin();
myMap.erase(it); // it失效
// 不能再使用it
同樣,只有被刪除元素的迭代器會失效,其他元素的迭代器仍然有效。
四、如何避免迭代器失效的坑?
知道了問題所在,我們該如何避免呢?這里有幾個實用技巧:
技巧一:使用 erase 和 insert 的返回值
大多數(shù)容器的 erase 方法都會返回下一個有效迭代器,insert會返回指向新插入元素的迭代器,我們可以利用這一點。
// erase的返回值
vector<int> nums = {1, 2, 3, 4, 5};
for (auto it = nums.begin(); it != nums.end(); ) {
if (*it == 3) {
it = nums.erase(it); // erase返回下一個有效迭代器
} else {
++it;
}
}
// insert的返回值
vector<int> nums2 = {1, 2, 3, 4};
auto it2 = nums2.begin();
it2 = nums2.insert(it2 + 2, 100); // it2現(xiàn)在指向新插入的100
cout << *it2 << endl; // 輸出100
這個技巧在需要連續(xù)操作容器時特別有用,可以保持迭代器始終有效。
技巧二:先記錄再刪除
vector<int> nums = {1, 2, 3, 4, 5};
vector<int> toRemove;
// 先標記要刪除的元素
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] == 3) {
toRemove.push_back(i);
}
}
// 從后往前刪除(避免索引變化)
for (int i = toRemove.size() - 1; i >= 0; --i) {
nums.erase(nums.begin() + toRemove[i]);
}
技巧三:使用穩(wěn)定的容器操作
一些容器操作不會導致迭代器失效,可以優(yōu)先使用這些操作。
// 對于list,splice操作不會導致迭代器失效
list<int> myList = {1, 2, 3, 4, 5};
list<int> anotherList = {10, 20};
auto it = myList.begin();
++it; // it指向2
myList.splice(myList.end(), anotherList); // 不會導致it失效
cout << *it << endl; // 仍然是2
五、實戰(zhàn)案例:解決常見迭代器失效問題
案例一:刪除 vector 中的偶數(shù)
錯誤寫法:
vector<int> nums = {1, 2, 3, 4, 5, 6};
for (auto it = nums.begin(); it != nums.end(); ++it) {
if (*it % 2 == 0) {
nums.erase(it); // 錯誤!迭代器失效
}
}
正確寫法:
vector<int> nums = {1, 2, 3, 4, 5, 6};
for (auto it = nums.begin(); it != nums.end(); ) {
if (*it % 2 == 0) {
it = nums.erase(it);
} else {
++it;
}
}
案例二:在遍歷的同時添加元素
錯誤寫法:
vector<int> nums = {1, 2, 3};
for (auto it = nums.begin(); it != nums.end(); ++it) {
if (*it == 2) {
nums.push_back(4); // 錯誤!可能導致迭代器失效
}
}
正確寫法(使用下標):
vector<int> nums = {1, 2, 3};
int size = nums.size(); // 先記錄原始大小
for (int i = 0; i < size; ++i) {
if (nums[i] == 2) {
nums.push_back(4); // 使用索引而非迭代器
}
}
六、總結(jié)
迭代器失效看起來很復雜,但只要記住幾個簡單的規(guī)則,就能輕松避開這個坑:
- vector: 插入或刪除元素后,該位置及其后面的迭代器都會失效;如果重新分配內(nèi)存,所有迭代器都會失效。
- list/forward_list: 只有被刪除元素的迭代器會失效。
- map/set/multimap/multiset: 只有被刪除元素的迭代器會失效。
- unordered_map/unordered_set: 插入操作可能導致所有迭代器失效(rehash);刪除操作只會導致被刪除元素的迭代器失效。
實際編程中,優(yōu)先考慮使用現(xiàn)代 C++ 的算法和容器操作,比如remove_if和erase的組合,往往能更優(yōu)雅地解決問題:
vector<int> nums = {1, 2, 3, 4, 5, 6};
// 一行代碼刪除所有偶數(shù)
nums.erase(remove_if(nums.begin(), nums.end(),
[](int x) { return x % 2 == 0; }),
nums.end());
怎么樣,迭代器失效這個坑,你現(xiàn)在是不是已經(jīng)有底了?下次寫代碼的時候,別忘了提醒自己:容器變了,迭代器可能就不靠譜了!