在上一章我們探討了C++的類型系統(tǒng),并提出了從低到高,又從高到低的學(xué)習(xí)思路,本文就是一篇從高到低的學(xué)習(xí)指南,希望能提供一種新的視角。
編程語(yǔ)言一般分為兩個(gè)部分,一部分是語(yǔ)法部分,如上一章的類型系統(tǒng),另一部分則是用這套語(yǔ)法完成的預(yù)定義的工具集,如本文的主題——標(biāo)準(zhǔn)庫(kù)。標(biāo)準(zhǔn)庫(kù)是一堆我們寫代碼時(shí)直接可以用的代碼,就像是我們提前寫好的一樣,不僅如此,標(biāo)準(zhǔn)庫(kù)還是跨平臺(tái)的,還是經(jīng)過(guò)工業(yè)級(jí)測(cè)試的,所以標(biāo)準(zhǔn)庫(kù)有著靠譜,安全的特點(diǎn)。
C++標(biāo)準(zhǔn)庫(kù)包括很多方面,有類vector、string等,有對(duì)象std::cin,std::cout等,還有函數(shù)move,copy等,所以一般按功能來(lái)對(duì)它們分類
容器類
算法類
智能指針
線程相關(guān)
其他
當(dāng)然,這些還不是全部,標(biāo)準(zhǔn)庫(kù)是在不斷擴(kuò)充和完善的,學(xué)習(xí)標(biāo)準(zhǔn)庫(kù)的宗旨也應(yīng)該是學(xué)習(xí)它們的使用場(chǎng)景,而不是深入用法。比如容器類中就有很多功能類似的類,不同的業(yè)務(wù)場(chǎng)景有不同的選擇。通過(guò)對(duì)它們的了解,我們更容易寫出高效,簡(jiǎn)潔的代碼。
容器類就是幫助管理一組數(shù)據(jù)的類,根據(jù)實(shí)現(xiàn)方式的不同,分為有序列表,無(wú)序列表和映射。
有序列表中的有序是指,數(shù)據(jù)組保存在一塊連續(xù)的內(nèi)存區(qū)域里,可以通過(guò)插入時(shí)的索引直接定位到原數(shù)據(jù)。因?yàn)閿?shù)據(jù)是按順序存入的,所以中途假如需要?jiǎng)h除或者新增數(shù)據(jù),在操作位置右邊的數(shù)據(jù)都需要移動(dòng),操作的代價(jià)就比較大。由此也可看出它們的優(yōu)勢(shì)是順序插入和尾部修改,還有直接查找,這方面的代表就是array,vector。array是對(duì)原始數(shù)組的封裝,并且解決了傳遞數(shù)組變成指針這樣的問(wèn)題,但是缺點(diǎn)是它的大小是固定的,適合用在數(shù)據(jù)量已知的情況。而vector又是對(duì)array的增強(qiáng),不僅能完成所有array的操作,并且大小可變,所以絕大部分情況下,選擇vector都是理想的選擇。

list,forward_list。顯然,有序列表和無(wú)序列表是互補(bǔ)的,我們?cè)趯?shí)際項(xiàng)目中,應(yīng)該根據(jù)數(shù)據(jù)的操作來(lái)確定選擇哪種容器。
映射則融合了有序列表和無(wú)序列表的優(yōu)點(diǎn),既可以快速插入和刪除,又可以快速查找。為了滿足各種使用場(chǎng)景,C++提供了map,multimap,unordered_map,unordered_multimap。從名字上就能看出來(lái)它們的差別。為了直觀,我直接列了一個(gè)表
| 是否排序 | 是否支持相同值 | 速度 | |
|---|---|---|---|
| unordered_map | ? | ? | ???????? |
| map | ? | ? | ???? |
| multimap | ? | ? | ?? |
| unordered_multimap | ? | ? | ?????? |
映射存儲(chǔ)的是兩個(gè)值,不同的類型實(shí)現(xiàn)方式不一樣。由于map是需要排序的,所以通常它的實(shí)現(xiàn)是一種平衡二叉樹,鍵就是它排序的依據(jù)。

unordered_map是不需要排序的,所以它的實(shí)現(xiàn)通常是哈希表,即根據(jù)哈希函數(shù)的確定索引位置繼而確定存儲(chǔ)位置。
vector都是靠譜的選擇,它提供了全功能的數(shù)據(jù)操作接口,支持動(dòng)態(tài)長(zhǎng)度,索引查詢,并且簡(jiǎn)單高效。如果需要頻繁地插入或者刪除操作,也可以考慮list或者forward_list。map可以讓數(shù)據(jù)保持有序,需要更快的速度而不是排序的話unorderer_map是更好的選擇,如果相同值會(huì)出現(xiàn)多次就可以使用對(duì)應(yīng)的multi版本。另外容器類也是很好的數(shù)據(jù)結(jié)構(gòu)學(xué)習(xí)資源,C++的容器類幾乎提供了數(shù)據(jù)結(jié)構(gòu)中所有的形式,對(duì)數(shù)據(jù)結(jié)構(gòu)越熟悉選擇的容器類就越完美。之所以將算法放在容器類后面,是因?yàn)樗惴ù蟛糠质菍?duì)容器類操作的加強(qiáng),算法都定義在algorithm文件頭里。這些算法都是短小精悍的,可以大大增加代碼可讀性,并且妥善處理了很多容易遺忘的邊界問(wèn)題。功能上可以分為增刪改查幾種操作,可以在實(shí)際有需要的時(shí)候在查看文檔,具體可以參閱這里
很早以前,我對(duì)智能指針的態(tài)度不是很好。因?yàn)閯傞_(kāi)始學(xué)習(xí)C++時(shí)我就知道,不能單獨(dú)使用指針,要把指針?lè)庋b在類里,利用類的構(gòu)造函數(shù)和析構(gòu)函數(shù)管理指針,也就是RAII。最開(kāi)始我以為這就夠了,直到我遇到下面這種情況
1public:
2????Ptr():p{?new?int?}?{}
3????~Ptr()?{
4????????delete?p;
5????}
6????int&?get()?{
7????????return?*p;
8????}
9
10????void?set(const?int?value)?{
11????????*p?=?value;
12????}
13private:
14????int*?p;
15};
16
17void?use(Ptr?p)?{
18????//傳進(jìn)來(lái)的是復(fù)制構(gòu)造出來(lái)的p',函數(shù)返回后p'被銷毀啦,兩個(gè)指針指向的地址被回收,外面的p指針成為了野指針
19}
20int?main()?{
21????Ptr?p;
22????p.set(1);
23????use(p);?//p按值傳遞,調(diào)用了Ptr的復(fù)制構(gòu)造函數(shù),構(gòu)造出了新對(duì)象p',它的指針和p的指針指向同一個(gè)地方
24????std::cout?<std::endl;?//p已經(jīng)被銷毀了,訪問(wèn)p的地址非法
25????return?0;
26}
調(diào)用use時(shí),變量p被拷貝,也就出現(xiàn)了兩個(gè)指針同時(shí)指向一塊內(nèi)存地址的情況。use函數(shù)執(zhí)行完后,它的參數(shù)p被回收。也就是調(diào)用了Ptr的析構(gòu)函數(shù),也就是兩個(gè)指針指向的地址被回收。所以24行調(diào)用get讀取那個(gè)已經(jīng)被回收了的地址就是非法操作,程序崩潰。
這可能是新手比較常遇到的一個(gè)問(wèn)題,當(dāng)然,解決這個(gè)問(wèn)題也很簡(jiǎn)單,還用不到智能指針,只需要將函數(shù)use的參數(shù)改為引用類型就可以了,因?yàn)橐弥皇莿e名,不會(huì)產(chǎn)生新的指針,這也是我在類型系統(tǒng)篇中極力推薦引用為首選參數(shù)類型的原因之一。對(duì)于此例,數(shù)據(jù)不大,直接重寫復(fù)制構(gòu)造函數(shù),重新申請(qǐng)一塊內(nèi)存也是一種思路。
此例中用到Ptr的地方只有一個(gè),實(shí)際項(xiàng)目中Ptr往往需要用到很多次,我們不能保證不會(huì)出現(xiàn)忘記使用引用類型的情況,這種情況下重新申請(qǐng)內(nèi)存也不適用,所以這個(gè)時(shí)候就需要智能指針來(lái)幫忙了。
現(xiàn)在思考另一種情況,某些操作我們不得不暴露出我們的指針供外部使用,隨著業(yè)務(wù)的嵌套和調(diào)用鏈增加,很多時(shí)候會(huì)忘記或者不確定在什么時(shí)候調(diào)用delete釋放內(nèi)存。這也是用智能指針的一個(gè)場(chǎng)景。以上兩種情況都是需要分享指針,對(duì)應(yīng)智能指針中的shared_ptr。shared_ptr顧名思義,它可以幫助開(kāi)發(fā)者完成指針共享的問(wèn)題,并且完美解決提前釋放,不知何時(shí)釋放,誰(shuí)負(fù)責(zé)釋放的問(wèn)題。它的對(duì)應(yīng)關(guān)系是一對(duì)多,一個(gè)實(shí)際的內(nèi)存可以被多個(gè)shared_ptr共享

unique_ptr。
unique_ptr的對(duì)應(yīng)關(guān)系是一對(duì)一,無(wú)論哪個(gè)時(shí)刻,只能有一個(gè)管理者擁有指針,也就只能由它負(fù)責(zé)釋放了。假如想轉(zhuǎn)移這種對(duì)應(yīng)關(guān)系,只能通過(guò)std::move操作,不過(guò)這個(gè)操作之后,原先對(duì)象的指針就失效了,它也不再負(fù)責(zé)管理,所有的任務(wù)移交給了新的對(duì)象。這種特性特別適合資源敏感型的應(yīng)用。除了內(nèi)存,線程是開(kāi)發(fā)中另一個(gè)重要的課題。線程的難點(diǎn)在于不僅要管理線程對(duì)象,還要管理線程對(duì)象管理的資源,并且保證線程間數(shù)據(jù)同步。當(dāng)然標(biāo)準(zhǔn)庫(kù)已經(jīng)做得足夠好了,我們需要理解的是使用場(chǎng)景的問(wèn)題。線程庫(kù)主要包括線程對(duì)象thread,條件對(duì)象condition_variable,鎖對(duì)象mutex。
使用thread可以很方便地把程序?qū)懗啥嗑€程,只需要三步:
1void?plus(int?a,int?b){?//第一步:定義線程中要運(yùn)行的函數(shù)
2????std::cout<<"running?at?sub?thread"<<std::endl;
3????std::cout<<"a?+?b?=?"<std::endl;
4}
5
6int?main(){
7????std::thread?thread{plus,1,1};?//第二步,定義std::thread對(duì)象,將函數(shù)作為參數(shù)
8????std::cout<<"continue?running?at?main?thread"<<std::endl;
9????thread.join();?//第三步調(diào)用線程對(duì)象的join函數(shù)或者detach函數(shù)
10????std::cout<<"sub?thread?finished!"<<std::endl;
11}
12//輸出
13//????continue?running?at?main?thread
14//????running?at?sub?thread
15//?????a?+?b?=?2
16//?????sub?thread?finished!
難點(diǎn)在線程間通信,也就是解決兩個(gè)問(wèn)題
線程1更新了變量v的值
線程2馬上能讀取到正確的變量v的值,即線程1更新的那個(gè)最新值
為了協(xié)調(diào)這兩個(gè)過(guò)程,就出現(xiàn)了鎖對(duì)象mutex和條件對(duì)象condition_variable。鎖對(duì)象mutex保證變量按照正確的順序更改。條件對(duì)象condition_variable保證更改能被其他線程監(jiān)聽(tīng)到。
1int?a,b;
2bool?ready?=?false;
3std::mutex?mux;
4std::condition_variable?con;
5
6void?plus()?{
7????std::cout?<"running?at?sub?thread"?<std::endl;
8????//因?yàn)槲覀円x取ready的最新值,所以要用鎖保證讀取結(jié)果的有效性
9????std::unique_lock<std::mutex>?guard{?mux?};
10????if?(!ready)?{
11????????//數(shù)據(jù)沒(méi)準(zhǔn)備好,休息一下!
12????????con.wait(guard);?
13????}
14????//這里就可以正確讀變量a,b了
15????std::cout?<"a?+?b?="?<std::endl;
16}
17
18int?main()?{
19????std::thread?thread{?plus};
20????std::cout?<"continue?running?at?main?thread"?<std::endl;
21????std::cout?<"input?a?=?";
22????std::cin?>>?a;
23????std::cout?<"input?b?=?";
24????std::cin?>>?b;
25????{
26????????//數(shù)據(jù)準(zhǔn)備好了,該通知子線程干活了,用大括號(hào)是因?yàn)橄胱屾i因?yàn)間uard的銷毀即使釋放,從未保證plus里面能重新獲得鎖
27????????std::unique_lock<std::mutex>?guard{?mux?};
28????????//更新數(shù)據(jù)
29????????ready?=?true;
30????????//通知
31????????con.notify_all();
32????}
33????thread.join();
34????std::cout?<"sub?thread?finished!"?<std::endl;
35}
多線程另一個(gè)需要注意的問(wèn)題就是死鎖。死鎖的前提是有兩個(gè)鎖
線程1得到了鎖a,還想得鎖b
線程2得到了鎖b,還想得鎖a
然后,再加上一個(gè)前提:某一時(shí)刻,只有一個(gè)線程能擁有某個(gè)鎖,就不難得出以下結(jié)論:線程a,b除非某一個(gè)放棄已得的鎖,不然兩個(gè)線程都會(huì)因?yàn)闆](méi)得到需要的鎖而一直死等,形成死鎖。同時(shí)解決死鎖的思路也呼之欲出:既然一個(gè)得了a,一個(gè)得了b,而鎖同一時(shí)間只能被一個(gè)線程得到,那么所有線程都按先得a,再得b的順序來(lái)就不會(huì)有鎖被占用的問(wèn)題了。另一個(gè)思路則可以從放棄上入手,既然都得不到,那么接下來(lái)的任務(wù)也做不了,不如直接放棄已經(jīng)得到的,所以可以考慮使用timed_mutex。
還有很多常用的庫(kù),如字符串string,時(shí)間chrono,還有在定義函數(shù)變量時(shí)常用的functional,異常exception,更多的內(nèi)容可以在cplusplus找的參考。
總的來(lái)說(shuō),標(biāo)準(zhǔn)庫(kù)提供了一個(gè)展現(xiàn)C++語(yǔ)言能力的平臺(tái):幫助開(kāi)發(fā)者更好更快完成開(kāi)發(fā)任務(wù)的同時(shí),還能啟迪開(kāi)發(fā)者實(shí)現(xiàn)更好的抽象和實(shí)踐。如我就從標(biāo)準(zhǔn)庫(kù)中學(xué)到了更規(guī)范地定義函數(shù)參數(shù),更好的封裝,以及其他好的思路。學(xué)習(xí)標(biāo)準(zhǔn)庫(kù)不僅更好地掌握了語(yǔ)言本身,還掌握了更全面地分析問(wèn)題,解決問(wèn)題的方法,是值得花費(fèi)一段時(shí)間學(xué)習(xí)的。
容器類是幾乎所有項(xiàng)目都會(huì)用到的,也是比較好掌握的,主要可以從數(shù)據(jù)結(jié)構(gòu)方面對(duì)照學(xué)習(xí);智能指針則是處理指針問(wèn)題的好幫手;線程相關(guān)的庫(kù)是比較難掌握的,關(guān)鍵是要想明白使用場(chǎng)景和極端情況下的邊界問(wèn)題。很多時(shí)候邊界問(wèn)題可能不那么直觀。如線程要求獲得鎖的情況就分為:鎖空閑,鎖被其他線程占有,鎖被自己占有。不同的邊界對(duì)于不同的鎖,預(yù)期結(jié)果也是不同的,只有在明確場(chǎng)景的情況下,才能更好地理清鎖的關(guān)系,從而解決好問(wèn)題。
最好的學(xué)習(xí)還是在實(shí)踐中主動(dòng)使用。對(duì)于我,通常在遇到新問(wèn)題的時(shí)候會(huì)先查查標(biāo)準(zhǔn)庫(kù)有沒(méi)有相應(yīng)的庫(kù),有的話就是學(xué)習(xí)這個(gè)庫(kù)的好時(shí)機(jī)??梢韵雀庞[庫(kù)的定義和解決的問(wèn)題,然后分析它提供的類,函數(shù),對(duì)象等,再將自己的理解轉(zhuǎn)換為項(xiàng)目中的代碼,最后在實(shí)際效果中檢驗(yàn)和修正想法,完成庫(kù)的學(xué)習(xí)。