1. 程式人生 > >【C++】C++避坑經驗談:陣列、vector

【C++】C++避坑經驗談:陣列、vector

要給新人培訓C++,奈何大家的時間是分散的,所以在這裡開坑寫文章了。這裡只是個人經驗,如果我也坑了的話請勿噴。

一、指標和陣列很危險?
  流行的說法是C++儘量不要用指標和陣列。指標會出現各種bug。當然只是說盡量不要用,不是說不用。在小型專案和追求速度的專案中,用指標並沒有太大問題。注意釋放並置零指標、不要用懸垂指標、避免陣列越界,指標用起來並沒有太大問題。但是遇到中大型專案,經驗豐富的程式設計師會出現指標錯誤使用的情況,是很正常。
  在專案中,我們會盡量減少指標使用,比如:函式傳參使用引用傳遞;使用智慧指標;仿照STL的迭代器,對指標進行包裹。
  注意的是,如果只是臨時使用指標,很快就會處理掉這個指標的話,用一用根本無關緊要,不能實行教條主義。另外需要注意的是,智慧指標std::shared_ptr包含的兩個物件,如果物件內部有指向對方的std::shared_ptr指標,是會出錯的,原因可以從std::shared_ptr的實現——引用計數中去思考。按照傳統的方法,相互指向對方的指標,其中一方可以使用std::shared_ptr指標,另一方需要使用std::weak_ptr指標。在開源三維影象庫VTK中,vtk的兩大類物件集合:processObject和dataObject內部有相互指向對方的指標。VTK是processObject包含指向dataObject的智慧指標,dataObject內部直接使用指向processObject的普通指標。這是一種合理的方法。

二、影象儲存用陣列還是std::vector?
  官方的說法是多用std::vector。然而對於影象處理的程式設計師來說,影象資料應該使用陣列來儲存。
  Debug模式用來進行程式除錯,尋找詳細的出錯資訊;Release模式可以用glog日誌、printf輸出程式碼資訊等方法來獲取出錯資訊,但明顯不如Debug模式靠譜。release程式的std::vector有時候可以達到原生陣列的速度,一些第三方STL庫內部就是用陣列來實現std::vector的;然而在Debug模式下,各平臺上std::vector比陣列慢多了。影象處理程式中,快速遍歷影象是基本操作之一,如果我們要用Debug模式除錯程式,顯然用遍歷std::vector儲存的影象,會慢的發狂。因此對於追求速度的一些實現,陣列是優先選擇。
  然而前面說了,C++要少用普通指標。因此我們一般需要用C++類對指向影象資料的陣列進行包裹。OpenCV庫的C和C++模式分別使用lpImage和Mat兩種資料格式,其中lpImage是用C-struct結構體對影象資料指標和其他資訊的包裝,但是依然需要使用專門的函式來刪除它;Mat是基於C++ 類class/struct封裝的影象資料,使用方法跟基本資料型別如int一樣,基本無需關心指標和記憶體方面的問題。C++類使用建構函式來自動建立影象陣列和相關資訊,解構函式可以在退出作用域時自動呼叫來刪除資料。這使得C++可以構造資源管理類來管理大量的資料。
  PS:在C++中,struct和class的區別在於,struct沒有指定的成員都是public,而class沒有指定的成員都是private;C++的struct包含預設建構函式和預設解構函式,而C語言的struct僅僅是一個數據集合,兩者是不同的。

三、std::vector什麼時候使用?
  與之相關的問題是:如何選擇STL的容器?
  從問題二中我們看到,std::vector不能用在需要高速讀寫遍歷的情況下。除去上面的情況,在不需要頻繁插入刪除資料序列頭部和中間元素的情況下,不需要使用關聯容器的情況下,陣列統統用std::vector來實現。
  std::vector有強大的錯誤檢查,用lazy-operation來實現快速尾部資料擴容。在基本確定資料數量的情況下,我們可以使用resize/reserve函式來預分配記憶體,提高速度。另外如果只是少量的中間資料的插入、刪除,std::vector的速度並沒有想象中那麼差,無需顧忌書中說的std::vector在中間資料的操作效能很不好啊之類的。
  下面這種情況,我們基本可以忽略std::vector對中間資料操作效能很差的特性,看如下程式碼(C++11):
  
vector<vector<double> > a;
for (int i = 0; i < 7; ++i) {
vector<double> b = { i+0.1, i+0.2, i+0.3 };
a.push_back(b);
}
cout << a.size() << endl;
double *p = &a[4][1];
cout <<p << ":" <<*p << endl;
a.erase(a.begin());
a.erase(a.begin());
cout << a.size() << endl;
cout << p << ":" << *p << endl;
  


輸出結果為:
7
00000000002B6B28:4.2
5
00000000002B6B28:4.2
這說明巢狀vector,外層vector儲存的是內層vector的指標引用,在刪除內層vector元素時,外層vector只是刪除了指向內層vector的指標引用並呼叫了該內層vector元素的解構函式,其他內層vector元素在記憶體中的位置根本就沒有動,因此效能開銷不大,可以放心用。