說到vector
怒竿,想必讀者都十分熟悉了帮掉,幾乎所有C++
程序員都會使用它表箭,不過許多人并不清楚真正的語義兔乞,無意間會犯一些很奇怪的錯誤弟断,今天看幾個關(guān)于vector
的問題直颅,當(dāng)然不可能把vector
所有的東西都拿出來講念祭,否則就變成討論vector
的實現(xiàn)了脯爪。
1. 一個簡單的例子
下面代碼中三幻,A
和B
兩行代碼有何區(qū)別就漾?
void simple(std::vector<int> v) {
v[0]; // A
v.at(0); // B
}
破冰
上面A
和B
兩行代碼都是在訪問v
的第一個元素,區(qū)別如下
- 當(dāng)
v
非空赌髓,則沒有區(qū)別从藤; - 當(dāng)
v
為空,B
會拋出一個std::out_of_range
锁蠕,至于A
的行為夷野,標(biāo)準(zhǔn)未作出聲明。
再探
結(jié)合這兩個函數(shù)在標(biāo)準(zhǔn)庫里的聲明看看荣倾,我稍稍的改寫下悯搔,方便閱讀,不影響理解
reference at(size_type __n);
reference operator[](size_type __n) noexcept;
從聲明我們可以看到兩個函數(shù)都是返回容器中第 n(參數(shù))個位置的元素的引用舌仍,它們還有兩個返回const
引用的版本妒貌。operator[]
是不會拋出異常的。使用成員函數(shù)at
去訪問vector
里面的元素铸豁,會先進行下標(biāo)越界檢查灌曙,當(dāng)越界發(fā)生將拋出out_of_range
的異常。但標(biāo)準(zhǔn)并未強制要求operator[]
做下標(biāo)檢查节芥,一個原因設(shè)計vector
是為了代替數(shù)組的在刺,對operator[]
效率要求很高。當(dāng)你需要顯示檢查下標(biāo)头镊,請使用at
成員函數(shù)蚣驼。
相關(guān)話題
在C++2.0
之后引入了std::array
來代替內(nèi)置數(shù)組,下表簡單總結(jié)了它們之間的差異
容器 | 底層數(shù)據(jù)結(jié)構(gòu) | 時間復(fù)雜度 | 其他 |
---|---|---|---|
array | 數(shù)組 | 隨機讀改 O(1) | 支持隨機訪問 |
vector | 數(shù)組 | 隨機讀改相艇、尾部插入颖杏、尾部刪除 O(1);頭部插入坛芽、頭部刪除 O(n) | 支持隨機訪問 |
2. 考慮 reserve
考慮下面的例子留储,會有什么問題
std::vector<int> v;
v.reserve(2);
v[0] = 1;
std::cout << v[0];
先看看上面第二行調(diào)用reserve
保證v
的容量capacity
大于等于2
翼抠,事實上很可能大于2
,因為vector的大小呈指數(shù)速度上升获讳。
問題比較明顯出在最后兩行机久,但是可能不易發(fā)覺,甚至在有些編譯器上 “勉強” 能夠 “正常運行”赔嚎。
問題出在混淆了size
和capacity
的概念。我們先理清下面兩個概念
-
size
與capacity
size用來指示容器當(dāng)前的元素個數(shù)胧弛;capacity表示容器的容量尤误,一般大于size,告訴你一般最少添加多少個元素才會導(dǎo)致容器重新分配內(nèi)存结缚。 -
resize
和reserve
resize是改變?nèi)萜鞯拇笮∷鹞睿以趧?chuàng)建對象;
reserve表示容器預(yù)留空間红竭,不會創(chuàng)建對象尤勋,只修改capacity大小,不修改size大幸鹣堋最冰;
所以在調(diào)用第二行代碼之后,v
仍然是空的稀火。但是標(biāo)準(zhǔn)并未強制要求operator[]
做下標(biāo)檢查暖哨,所以很可能在你的編譯器中會出現(xiàn)v[0] = 1;
被認(rèn)為是正確的情況,最后在標(biāo)準(zhǔn)輸出上打出1
凰狞,跟 "錯誤的" 預(yù)期相符合篇裁。
強調(diào)一下,上述的情形只是一種典型的可能情況赡若,并不一定會出現(xiàn)在所有地方达布。
3. 再看reserve
如果我們在2
的后面再加上下面這兩句,會出現(xiàn)什么情況
v.reserve(100);
std::cout << v[0];
-
一種可能的情況
接著之前的典型(錯誤的)情況逾冬,這個時候輸出的值可能為0
黍聂,不必詫異,剛剛賦值的1
去哪了粉渠。 -
解釋
假定第一次reserve(2)
并沒有使內(nèi)部緩沖區(qū)擴大到100或者更大分冈,這里reserve(100)
就會引入一次內(nèi)部緩沖區(qū)的重新分配,這時v
的元素會被復(fù)制到新分配的緩沖區(qū)中霸株,而問題是此時v
中根本沒有元素雕沉,空空如是,因此不會復(fù)制任何元素去件,此外坡椒,新分配的緩沖區(qū)初值可能為0
(嚴(yán)格來說不確定是0扰路,這里我們只是假設(shè)),因此就出現(xiàn)了上面的情況倔叼。 -
替代方案
將上面的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;
}
上面的程序正常運行沒有任何問題丈攒,有些小細(xì)節(jié)需要注意
-
盡量使用
!=
盡量使用!=
而不是<
來比較兩個迭代器哩罪。因為<
只對隨機訪問迭代器有效,而!=
對任何迭代器都有效巡验。方便將來需要時改變?nèi)萜鞯念愋图什澹?code>std::list迭代器不支持<
。 - 盡量使用前置
++
- 盡量使用
const_iterrator
-
盡量使用
\n
代替endl
華麗分割線來了.......
-
使用標(biāo)準(zhǔn)庫算法
C++
標(biāo)準(zhǔn)庫提供了一百多種有用的算法显设,可以避免使用原始循環(huán)框弛。例如copy
,for_each
捕捂,transform
瑟枫,accumulate
...,
我們使用標(biāo)準(zhǔn)庫算法重寫上面的代碼
// 盡量使用標(biāo)準(zhǔn)庫算法而不是原始for循環(huán)
std::copy(v.cbegin(), v.cend(), std::ostream_iterator<int>(std::cout, "\n"));
-
C++2.0的福音
C++11
之后范圍for
語句的引入指攒,使得循環(huán)寫起來得心應(yīng)手慷妙,也不容易出錯
for (auto i : v) {
std::cout << i << "\n";
}
-
相關(guān)話題
C++14
以后,由于對lambda
表達(dá)式的增強幽七,使得其與標(biāo)準(zhǔn)庫算法相結(jié)合往往可以寫出更加簡短的代碼景殷,往往表現(xiàn)力更強,這里簡單舉個例子澡屡,
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節(jié)可調(diào)用對象猿挚,或者查閱其它資料。
End
獨樂樂不如眾樂樂驶鹉,大家學(xué)習(xí)到的好東西也可以分享出來绩蜻。