關於 C++ vector 的兩個小 tips
本來這篇文章標題我想起成《關於 vector 的兩個小坑》,後來想想,其實也不算是坑,還是自己對原理性的東西理解的沒做那麼透徹。工作中遇到的很多問題,後來歸根到底都是基礎不牢靠。
vector 擴容
這個問題很經典了,但還是不小心踩到。有一個需求是要對目標元素進行復制,而目標元素集合是儲存在vector
裡面,於是簡單思考下就有如下程式碼(大致含義):
void Duplidate(vector<Element>* element_list, Element* element) { element_list.push_back(*element); } void Process() { for (auto& package : package_list) { if (IsNeedDuplicate()) { Duplicate(element_list, package->element); } } }
看起來好像沒什麼問題,就是當前的package
物件是否滿足複製的要求,需要的話,就對package
的成員origin_element
進行復制。跑 UT 也正常,然後在測試的時候就 coredump 了。看 core 檔案就是掛在了複製的時候。這裡我一開始就沒明白,一個簡單的複製為什麼會有 coredump。
檢查了很久 element 複製的場景,甚至想要專門寫一個拷貝建構函式。最後才恍然大悟,origin_element
指標指向的就是element_list
裡面的元素,element_list
是整體流程的資料來源,packge
物件是封裝的中間處理物件。之前的開發人員為了方便,直接在package
物件上儲存了原始的element
指標,而這個指標指向的是一個 vector 裡的元素。而我新加的邏輯會往原始的 vector 裡面再新增元素,那麼就有可能導致 vector 擴容,而 vector 擴容會導致整體的複製,從而導致原來指向這些元素的指標都失效了,靠後的package
物件再去訪問origin_element
就產生了 coredump。
當然,從設計上來說,就不應該儲存指向 vector 元素的指標,但是這裡有太多舊程式碼牽涉,這裡就不做討論。
vector::erase()
起因是我在程式碼裡面新增瞭如下程式碼(大致):
void EraseElement(const vector<Element>::iterator& element_iter, vector<Element>& element_list) { while (element_iter != element_list.end()) { element_list.erase(element_iter); } }
然後 cr 的同學提出了一個疑問是element_iter
是const
不可變的,但是在函式裡有擦除了對應的元素,這裡會不會有問題?雖然 UT 都已經跑過了,但是這種寫法的確比較奇怪,於是就藉機學習了一下vector::erase()
的實現原理跟用法。
erase(iterator)
的實現原理其實不會改變iterator
,而是把後面的元素一個個往前移動,相當於是iterator
指向的元素本身發生了變化,所以可以用const
來修飾這個iterator
。但是這裡用cosnt &
其實是沒有錯但是無用的修飾,除了容易讓人誤判之外,其實沒有什麼實際用途。我之前是為了修正 cpplint 才把reference 改成 const reference。
另外erase
本身的確比較危險,主要還是erase
的時候iterator
本身沒發生變化,但是指向的元素變了,,在很多時候iterator
會自然地指向下一個元素,但是由於這是未定義的行為,這裡面可能會有不可預期的地方,所以最終改成顯示的獲取返回重新賦值(erase()
會返回下一個迭代器,但這一點常常被忽略),這樣就能保證安全性了。更安全更推薦的做法應該是使用remove_if()
這裡就不展開講了。
void EraseElement(vector<Element>& element_list, vector<Element>::iterator element_iter ) { while (element_iter != element_list.end()) { element_iter = element_list.erase(element_iter); } }