幾個關於vector的問題

說到vector,想必讀者都十分熟悉了,幾乎全部C++程序員都會使用它,不過許多人並不清楚真正的語義,無心間會犯一些很奇怪的錯誤,今天看幾個關於vector的問題,固然不可能把vector全部的東西都拿出來說,不然就變成討論vector的實現了。程序員

1. 一個簡單的例子

下面代碼中,AB兩行代碼有何區別?算法

void simple(std::vector<int> v) {
  v[0];       // A
  v.at(0);    // B 
}

破冰
上面AB兩行代碼都是在訪問v的第一個元素,區別以下數組

  • v非空,則沒有區別;
  • v爲空,B會拋出一個std::out_of_range,至於A的行爲,標準未做出聲明。

再探
結合這兩個函數在標準庫裏的聲明看看,我稍稍的改寫下,方便閱讀,不影響理解數據結構

reference at(size_type __n);
reference operator[](size_type __n) noexcept;

從聲明咱們能夠看到兩個函數都是返回容器中第 n(參數)個位置的元素的引用,它們還有兩個返回const引用的版本。operator[]是不會拋出異常的。使用成員函數at去訪問vector裏面的元素,會先進行下標越界檢查,當越界發生將拋出out_of_range的異常。但標準並未強制要求operator[]作下標檢查,一個緣由設計vector是爲了代替數組的,對operator[]效率要求很高。當你須要顯示檢查下標,請使用at成員函數。函數

相關話題
C++2.0以後引入了std::array來代替內置數組,下表簡單總結了它們之間的差別學習

容器 底層數據結構 時間複雜度 其餘
array 數組 隨機讀改 O(1) 支持隨機訪問
vector 數組 隨機讀改、尾部插入、尾部刪除 O(1);頭部插入、頭部刪除 O(n) 支持隨機訪問

2. 考慮 reserve

考慮下面的例子,會有什麼問題設計

std::vector<int> v;
v.reserve(2);
v[0] = 1;
std::cout << v[0];

先看看上面第二行調用reserve保證v容量capacity大於等於2,事實上極可能大於2,由於vector的大小呈指數速度上升。
問題比較明顯出在最後兩行,可是可能不易發覺,甚至在有些編譯器上 「勉強」 可以 「正常運行」。
問題出在混淆了sizecapacity的概念。咱們先理清下面兩個概念code

  • sizecapacity

size用來指示容器當前的元素個數;capacity表示容器的容量,通常大於size,告訴你通常最少添加多少個元素纔會致使容器從新分配內存。orm

  • resizereserve

resize是改變容器的大小,且在建立對象;
reserve表示容器預留空間,不會建立對象,只修改capacity大小,不修改size大小;對象

因此在調用第二行代碼以後,v仍然是空的。可是標準並未強制要求operator[]作下標檢查,因此極可能在你的編譯器中會出現v[0] = 1;被認爲是正確的狀況,最後在標準輸出上打出1,跟 "錯誤的" 預期相符合。
強調一下,上述的情形只是一種典型的可能狀況,並不必定會出如今全部地方。

3. 再看reserve

若是咱們在2的後面再加上下面這兩句,會出現什麼狀況

v.reserve(100);
std::cout << v[0];
  • 一種可能的狀況

接着以前的典型(錯誤的)狀況,這個時候輸出的值可能爲0,沒必要詫異,剛剛賦值的1去哪了。

  • 解釋

假定第一次reserve(2)並無使內部緩衝區擴大到100或者更大,這裏reserve(100)就會引入一次內部緩衝區的從新分配,這時v的元素會被複制到新分配的緩衝區中,而問題是此時v中根本沒有元素,空空如是,所以不會複製任何元素,此外,新分配的緩衝區初值可能爲0(嚴格來講不肯定是0,這裏咱們只是假設),所以就出現了上面的狀況。

  • 替代方案

將上面的v[0] = 1;替換成v.push_back(1);就不會有問題了,它老是會像容器的尾部追加元素。

4. 遍歷vector

思考一下下面的代碼片斷

for (vector<int>::iterator iter = v.begin(); iter < v.end(); iter++) {
  std::cout << *iter << std::endl;
}

上面的程序正常運行沒有任何問題,有些小細節須要注意

  • 儘可能使用!=

儘可能使用!=而不是<來比較兩個迭代器。由於<只對隨機訪問迭代器有效,而!=對任何迭代器都有效。方便未來須要時改變容器的類型,例如std::list迭代器不支持<

  • 儘可能使用前置++
  • 儘可能使用const_iterrator
  • 儘可能使用\n代替endl

華麗分割線來了.......

  • 使用標準庫算法

C++標準庫提供了一百多種有用的算法,能夠避免使用原始循環。例如copyfor_eachtransformaccumulate...,

咱們使用標準庫算法重寫上面的代碼

// 儘可能使用標準庫算法而不是原始for循環
std::copy(v.cbegin(), v.cend(), std::ostream_iterator<int>(std::cout, "\n"));
  • C++2.0的福音

C++11以後範圍for語句的引入,使得循環寫起來駕輕就熟,也不容易出錯

for (auto i : v) {
  std::cout << i << "\n";
}
  • 相關話題

C++14之後,因爲對lambda表達式的加強,使得其與標準庫算法相結合每每能夠寫出更加簡短的代碼,每每表現力更強,這裏簡單舉個例子,

int main() {
  std::vector<std::string> words{"One", "small", "step", "One", "big", "leap"};

  std::transform(begin(words), end(words), begin(words), [](const auto& word) {
    return "<" + word + ">";
  });
  std::for_each(begin(words), end(words), [](const auto& word) {
    std::cout << word << " ";
  });
}
// output
// <One> <small> <step> <One> <big> <leap>

不熟悉lambda的讀者能夠參考個人另外一篇文章第4節可調用對象,或者查閱其它資料。

End

獨樂樂不如衆樂樂,你們學習到的好東西也能夠分享出來。

相關文章
相關標籤/搜索