學習C++:實踐者的方法(Beta1)

前言

我的blog以前很長一段時間關注的都是C++中的技術&細節,乃至於讀者和應者都寥寥。然而5月份的時候寫的一篇“你應當如何學習C++”,閱讀量卻達到了3萬多,在blog上所有文章中卻是最高的(且遠遠超過了第二位);評論數目也有一百多。為什麼獨獨這篇能夠激起這麼多的迴應,想必是國內的C++社群被C++壓抑太久,或者,嚴格來說,是被C++的教育方式壓抑太久。實際上,不管是在各大國內論壇上,還是在comp.lang.c++.moderated這樣的國際C++論壇上,乃至於在douban上的小組內,有心者都會發現,對C++語言的細節的關注一直都沒有停止過,同樣,對C++語言的細節的抱怨也從來都沒有停止過。一個例子就是comp.lang.c++.moderated上的一個技術牛人James Kanze說的,他說接觸C++十年了,到現在還需要不時去翻C++標準。這就難怪Eric Raymond老大在《The Art of Unix Programming》中說“C++是反緊湊”的了。C++中的細節太多,就算都看過了,也不可能都記住。更關鍵的是,就算都記住了,也不能讓你成為一個真正的好程序員。

絕大多數人都把細節太多(或者用貶義詞來說就是“陰暗角落太多”)歸結為C++的本質問題,認為一切邪惡由此而生。也正因此,大約9月份的時候,Linus在郵件列表上說“C++是一門有思想包袱的語言;僅僅是為了讓程序員遠離C++,我也要用C”。這句短短的話在國內引起了很大的反應,最初是劉江轉了Linus的話,然後雲風和孟巖都發表了自己的看法;我也寫了一篇“Why C++”(後來發給Bjarne,Bjarne對這篇文章做了一個友情評註)。

然而,這一通渾水攪過之後,我相信引起的變化未必很大。大多數原先的反對者能從中找出反對的理由,於是更加反對;大多數原先的贊同者也能從中找到贊同的理由,於是更加贊同;而剩下來的原先沒有明確意見的,看雙方各有各的道理,可能還是沒有頭緒。

擺脫自我服務偏見——理性思考的前提

《決策與判斷》上提到過一個有趣的真實故事:1980年的某一天,美國空戰司令部的計算機突然發出警報——蘇聯的一枚核彈正在向美國本土飛來。司令部立即調兵遣將,迅速為一場核戰做好了準備,然而3分鐘之後,工程人員發現是計算機的一個小零部件故障造成的。然而,這場虛驚之後,大眾的反應才是真正有意思的:原先支持核武裝的,認為現在感覺更加安全了(因為“事實證明這類的故障是完全可克服的”);而原先反對核武裝的則認為更不安全了(因為“這類錯誤信號可能導致蘇聯過度反應,引發真正的核戰”)。類似的情況也發生在三裡島核洩露事件之後,同樣的,反對者認為(“這表明管理部門沒有辦法安全管理核能”),支持者認為(“這正表明這樣的危險沒有想像得那麼嚴重,是可克服的”)。社會心理學把諸如此類的現象總結為“自我服務偏見”。不幸的是,“真理越辯越明”其實只適用於理性思考者。

為什麼囉嗦這麼一大通呢?就是因為,一直以來氾濫於程序員社群的“語言之爭”,背後真正的原因其實並不在於語言實質上的優劣,而在於觀察者的眼睛。在觀察者的眼睛裡面,語言並非一門工具,而是自己花了N多時間(其中尤數C++為最)來“修煉”的技能,對於這樣的技能,被否定無疑等同於自己被否定。所以,從心理學上講,語言並不是工具(儘管一直有這麼一種呼籲),而是信仰。這樣的信仰在越是花得時間久的語言上越是激烈。有趣的是,幾乎所有的“熱鬧”的社群都有這樣的現象,Java、Python、Ruby…莫不如是;因為就算語言本身不復雜,程序員仍然還是要投入大量的精力去學習各種各樣的框架類庫(想想Java的那些框架?)。因此這些語言社區的信仰未必不比C++社群的強烈。

然而,一旦弄清我們為什麼會把語言當成信仰,就非常有助於擺脫在看待語言時的“自我服務偏見”,從客觀的角度去看待問題。——“當你看到的是支持某個意見的證據時,試著去想一想有哪些證據是不支持它的”。

那麼為什麼要擺脫自我服務偏見?說小了,是為了成為一個更優秀的程序員(誰也不希望因為偏見而去使用一門低效的語言乃至不妥當的語言)。說大了是節省生命(因為偏見可能導致越陷越深,浪費時間)。

所以,如果你能夠理性的思考我們將要討論的問題,避免自我服務偏見(就當你從來沒有花時間在C++上一樣)。那麼我們便可以開始討論真正的問題了。

前言2

現在,幾乎每個學習C++的都知道C++的核心問題是其複雜性;甚至本身不在C++社群的,也知道這是事實。群眾的眼睛是雪亮的,何況這還是個太顯而易見的事實。

但看了無數篇闡述C++複雜性的文章,和爭論C++複雜性的吐沫星子(包括我前段時間寫的兩篇關於C++的總結)。我始終都有一個感覺——沒分析透,就跟盲人摸象一樣。正如“Why C++”的一位讀者批評的,我在文章裡面沒有寫明到底哪些是C++的“非本質複雜性”。當然,我自己憑感覺就能知道,而接觸C++一段時間的人大致也能知道,但新手乃至非新手則對我所謂的“非本質複雜性”根本沒有一個具體的認識,這就使得那篇“Why C++”脫離了原本的意圖——面向所有C++使用者和學習者。

同樣的原因,在寫了“你應當如何學習C++”一文之後,當孟巖先生邀請我給《程序員》寫一個系列的文章,介紹一下我在接觸C++的過程中的態度和認識轉變時,我雖然非常高興的答應了,但直到現在3個月過去了還是顆粒無收。為什麼?因為我覺得真正本質的問題沒有被清晰的觸摸到;所以直到現在我都沒有動筆,免得廢話說了一大堆,除了能被當成小說讀讀之外,對真正考慮是否要學習乃至使用C++的人未必有什麼實際用處。

然而,這麼個念頭一直都放在潛意識裡面。前一陣子和Bjarne通信,談到了關於C++複雜性的一些想法,在郵件裡面總結了一下C++的複雜性來源,感覺思路清晰了許多。而這篇文章要達到的目的,正是傳達對C++的複雜性的一個具體而明確的認識,有了這個認識作為支持,我們便可以推導出學習C++的最佳(實踐者)的方法。

為什麼要學習(並使用)C++

顯然,如果找不出要學習C++的理由,那麼談什麼“正確的學習方法”等於是廢話。

首先重複一句Bjarne的話:“我們的系統已經是極度複雜的了,為了避開C++的複雜性而乾脆不用C++(Linus的做法),無異於因噎廢食。”在所有可用C和C++的領域,C++都是比C更好的語言。當我說“更好的”時候,我說的是C++擁有比C更安全的類型檢查、更好的抽象機制、更優秀的庫。當然,凡事都有例外,如果你做的項目1)不大。2)編碼中用不到什麼抽象機制,甚至ADT(抽象數據類型,例如std::complex這種不含多態和繼承的)也用不到,RAII也用不到,異常也用不到。3)你連基礎庫(如,簡化資源管理的智能指針、智能容器)都用不著。那麼也許你用C的確沒問題;所以如果你的情況如此,不用和我爭論,因為我無法反駁你。我們這裡說的領域大致是Bjarne在“C++應用列表”裡面列出來的那些地方。

底線是:如果把C++中的諸多不必要的複雜性去掉,留下那些本質的,重要的語言特性,簡化語言模型,消除歷史包袱。即便是C++的反對者也許也很難找到理由說“我還是不用C++”。在我看來,一個真正從實踐意義上理性反對使用C++的人只有一個理由:C++的複雜性帶來的混亂抵消乃至超過了C++的抽象機制和庫(在他的特定項目中)帶來的好處。

值得注意的是,這裡需要避免一個陷阱,就是一旦人們認定了“C++不好”,那麼這個理由就會“長出自己的腳來”,即,就算我們拿掉C++的複雜性,他們可能也會堅持還是不用C++,併為之找一堆理由。我假定你不是這樣的人。不過,也許最可能的是他會說:“問題是我們今天用的C++並非如此(簡潔),你的假設不成立。”是的,我的假設不成立。但雖然我們無法消除複雜性,我們實際上是可以容易地避開復雜性,避短揚長的。這也是本文的要點,容我後面再詳述。

當然,到現在你可能還是會說。我還是不用C++,因為我可以用D;或者如果你本來做的項目就不需要C++,你則可能會說,我用Python。首先,如果你的項目能用Java/Python乃至Ruby做,那麼用C++是自討苦吃。因為能用那些語言代表你的項目在效率上本身要求就不高,那麼用一門效率上討不到太大好處,複雜性上卻綽綽有餘的語言,有什麼價值呢?其次,如果你的項目效率是很重要的,你可能會說可以用D。然而現實是D在工業界尤其是國內被運用得非常少,幾乎沒有。而C++卻有大量的既有代碼,已經使用C++去做他們的產品的公司,在很長一段時間之內幾乎是不可能用別的語言重寫代碼的,正如Joel所說,決定重寫一個非平凡的代碼基==自殺。所以,我們至少要注意以下兩個明顯的事實:

事實1:C++在工業界仍有穩定的核心市場。

這個事實大概不需要多加闡述,很多大公司的核心技術還是要靠C++來支撐的(見Bjarne主頁上的C++應用列表)。所謂事實,就是未必是大家最願意承認的情況,但又不得不承認。C++積累了龐大的代碼基,這個代碼基不是一朝一夕能夠推翻的。D從語言角度來說的確優於C++,但最關鍵的就是還沒有深入工業界(也許根本原因是沒有錢支持,但這不是我們討論的重點)。而C呢,根據Bjarne本人的說法,他的觀察是主流工業界的趨勢一直是“從C到C++”的,而不是反過來,至少在歐美是如此。在國內我們則可以通過CSDN上的招聘情況得到一個大致類似的信息。

事實2:C++程序員往往能享受到有競爭力的薪酬。

是的,這不是一篇不食人間煙火的技術文章。這個事實基於的邏輯很簡單:物以稀為貴。Andrei Alexandrescu這次來中國SD2.0大會的時候,在接受採訪時也說過:“最賺錢的軟件(如MS Office)是C++寫的”。孟巖也在blog上提到這麼個事實,我想他作為CSDN的技術總編,業界觀察肯定比我清晰深刻。所以我這裡就不多廢話了。 當然,以上邏輯並不就意味著在慫恿你去學C++,一切還要看你的興趣。所以如果你志不在C++身處的那些應用領域,那這篇文章並非為你而寫。 “C++的複雜性是根本原因”——一個有漏洞的推理

一旦我們認識了C++在一些領域是有需求的(值得學習和掌握的)這個問題之後,就可以接下來討論“怎樣正確學習和掌握C++”這個核心問題了。

其實,對於這個問題,Bjarne已經宣傳了十年。早在99年的時候Bjarne就寫了“Learning C++ as A New Language”,並在好幾篇技術訪談(這裡,這裡,這裡,還有這裡)裡面提到如何正確對待和使用C++中支持的多種抽象機制的問題。Andrew Koenig也寫了一本現代C++教程《Accelerated C++》(這本書後面還會提到)。然而這麼多年來,C++社群的狀況改善了嗎?就我所知,就算有改善,也是很小的。學習者還是盲目鑽語言細節,只見樹木不見森林;網上還是瀰漫著各種各樣的“技術”文章和不靠譜的“學習C++的XX個建議”;一些業界的有身份的專家還是在一本接一本的出語言孔乙己的書(寫一些普通程序員八輩子用不著的技巧和碰不著的角落);而業界真正使用C++的公司在面試的時候還總是問一些邊邊角角的細節問題,而不是考察編程的基本素養(不,掌握所有的語言細節也不能讓你成為一個合格的程序員)。這個面試理念是錯誤的,估計其背後的推理應該是“如果這個傢伙不知道這個細節,那麼估計他對語言也熟悉不到哪兒去;而如果他知道,那麼雖然他可能並不是好的程序員,但我們還是能夠就後一個問題進一步測試的”,這個理念的問題在於,對語言熟悉到一定程度(什麼程度後面會具體建議)就已經可以很好的編程了(剩下的只需查查文檔);而很多公司在測試“對語言熟悉程度”的時候走得明顯太遠了(比如,問臨時對象生命期和析構順序當然是無可厚非的,但問如何避免一個類被拷貝或者如何避免其構建在堆上?);當然,有些語言知識是必須要提前掌握的,具體有哪些後面會提到,面試的時候並非不能問語言細節,關鍵是“問哪些”。 所以說:

事實3:C++的整個生態圈這麼些年來在學習C++的哲學上,實在沒有多少改善。

為什麼?是因為Bjarne介紹的學習方法在技術上沒有說到點子上?是Andrew Koenig的書寫得不夠好?說了誰也不會相信。因為實際上,這裡的原因根本不是技術上的,而是非技術的。 眾所周知的一個事實是,從最表層講,C++的最嚴重問題是在語言學習階段佔用了學習者的太多時間。翻一翻你的C++書架或者電子書目錄,絕大多數的C++“經典”都是在講語言。在我們通常的意義上,要“入門”C++,在語言上需要耗的時間一般要兩三年。而要“精通”C++,則搞不好需要耗上十年八年的。(這跟Peter Norvig說的“十年學習編程”其實不是一回事,人家那是說一般意義上的編程技能,不是叫你當語言律師。) 那為什麼我說“C++的複雜性是根本原因”是個有漏洞的推理呢?因為,要讓人們在使用一門語言去做事情之前耗上大量時間去學習語言中各種複雜性,除了語言本身的複雜性的事實之外,還有一個重要的事實,那就是學習者的態度和(更重要的)方法。而目前大多數C++學習者的態度和方法是什麼呢?——在真正用C++之前看上一摞語言書(日常編程八輩子都未必用得到)。而為什麼會存在這樣的學習態度呢?這就是真正需要解釋的問題。實際上,有兩方面的原因:

事實4:市面上的絕大多數C++書籍(包括很多被人們廣泛稱為“必讀經典”的)實際上都是反面教材。

也就是說,隨便你拿起哪本C++書籍(包括很多被人們廣泛稱為“必讀經典”的),那麼有很大的可能這本書中的內容不是你應該學的,而是你不應該學的。我之所以這麼說有兩個原因,因為一,我曾經是受害者。二,也是更實質性的原因,這些所謂的必讀經典,充斥的是介紹C++中的陷阱和對於C++的缺陷的各種workarounds(好聽一點叫Idioms(慣用法)或techniques(技術));又因為C++中的這類陷阱和缺陷實在數不勝數,所以就拉出了一個“長尾”;這類書籍在所有語言中都存在(“C缺陷和陷阱”、“Effective Java”、“Effective C#”等等),然而在C++裡面這個尾巴特別長,導致這類書數不勝數。三,這些書中列出來的缺陷和陷阱根本不區分常見程度,對於一個用本程序員來說,應該希望看到“從最常見的問題到最不常見的問題”這樣的順序來羅列內容,然而這些書裡面要麼全部混在一起,要麼按照“資源管理、類設計、泛型”這樣的技術分類來介紹內容,這根本毫無幫助(如果我看到一個章節的內容,我當然知道它講的是類設計還是資源管理,還用廢話麼?),使得一個學習者無法辨別並將最重要的時間花在最常見的問題之上。

最最關鍵的是:這些書當中介紹的內容與成為一個好程序員根本毫無關係,它們頂多隻能告訴你——嗨,小心跌入這個陷阱。或者告訴你——嗨,你知道當你(八輩子都不一定遇到)遇到這個需求的時候,可以通過這個技巧來得以解決嗎?結果讀了一本又一本之後,你腦袋裡除了塞滿了“禁止”、“警戒”、“燈泡”符號之外,真正的編程素質卻是一無長進。又或者有這樣一類書,熱衷於解釋語言實現背後的機制,然而語言特性本質上是幹嘛用的?是用來在實際編碼中進行抽象的(說得好聽一點就是“設計”),不是用來告訴你這個特性是怎麼支持的。比如我就見過以下的情景:面試官問:“你知道虛函數嗎?”得到的回答是一堆關於虛函數表機制的解釋。面試官又問:“那虛函數的好處是什麼呢?”到底為什麼要虛函數呢?得到的回答是:“恩…啊…就是…多態吧”(這時已經覺得回答不夠深刻了)。再問:“那多態是幹嘛的呢?”啞口無言。

事實5:就算記住一門語言的所有細節也不能讓你成為一個合格的程序員。

事實6:瞭解語言實現固然有其實踐意義(在極端場合的hack手法,以及出現底層bug的時候迅速定位問題),然而如果為了瞭解語言機制而去了解語言機制便脫離了學習語言的本意了。

在C++裡面這樣的情況很多見:知道了語言實現的底層機制,卻不知道語言特性本身的意義在什麼地方。本末倒置。為什麼?書害的。二,這類書當中介紹的所有情景加起來其實只屬於那20%(二八法則),甚至20%都不到的場景(究竟是哪些書,後面會介紹,我不便直接列出書名,打擊面太大,但我會把我認為essential的書列出來)。這就是為什麼我說“八輩子都用不著”的原因。

事實7:80%的C++書籍(包括一些“經典”)只涉及到20%(或者更少)的場景。

你可能會說,那難道這些書就根本不值得看了嗎?

我的回答是,對。根本不值得看。——但是值得放在旁邊作為必要的時候的參考(記住從索引或目錄翻起,只看嚴格必要的部分),如果你是個嚴肅的程序員的話。因為不管承認與否,墨菲法則的強大力量是不可忽視的——如果有一個可能遇到的陷阱,那麼總會遇到的。而同樣,C++的那些奇技淫巧也並非空穴來風,總有時候會需要用到的。但是你不需要預先把C++的所有細節和技巧存在腦子裡才能夠去編程,即:

建議1:有辨別力地閱讀(包括那些被廣泛稱為“經典”的)C++書籍。

如果書中介紹的某塊內容你認為在日常編程中基本不會用到(屬於20%場景),那麼也許最好的做法是非常大概的瀏覽一下,留個印象,而不是順著這條線深究下去。關於在初學的時候應該讀哪些書,後面還會提到。 實際上,除了語言無關的編程修養之外(需要閱讀什麼書後面會提到),對於C++這門特定的語言,要開始用它來編程,你只需知道一些基礎但重要的語言知識(需要閱讀哪些書後面會提到)以及“C++裡面有許多缺陷和陷阱”的事實,並且——

建議2:養成隨時查閱資料和文檔的習慣。

“查文檔”幾乎可以說是作為一個程序員最重要的能力(是的,能力)了;它是如此重要,以至於在英文裡面有一個專門的縮寫——RTFM。為什麼這個能力如此重要,原因很簡單:編程領域的知識太雞零狗碎了。不僅知識量巨大,而且知識的細節性簡直是任何學科都無與倫比的(隨便找一個框架類庫看看它的API文檔吧)。所以,把如此巨量的信息預先放在腦子裡不僅不實際,而且簡直是自作孽。你需要的是“元能力”,也就是查文檔的能力——從你手頭遇到的問題開始,進行正確合理的分析,預測問題的解決方案可能在什麼地方,找到關於後者的資料,閱讀理解,運用。

同樣,在C++中也是如此,如果你從學習C++一開始就抱著這種態度的話,那麼即便等到面試的時候被問到某個語言細節,你也可以胸有成竹的說你雖然並不知道這個細節,但在實際編碼中遇到相應問題的時候肯定會找到合適的參考資料並很快解決問題(解決問題,才是最終目的)。當然,更大的可能性是,你在平常編碼中已經接觸過了最常見的那80%的陷阱和技巧了,由於你用的是實踐指導性的學習方式,所以你遇到的需要去學習的陷阱和技巧幾乎肯定都是常見場景下的,比沒頭蒼蠅似的逮住一本C++“經典”就“細細研讀”的辦法要高效N倍,因為在沒有實踐經驗的情況下,你很可能會認為其中的每個技巧,每個陷阱,都是同樣概率發作的。

為什麼市面上的C++書熱衷於那些細節和技巧呢?

你用一個天生用來開啤酒瓶的工具開了啤酒瓶,不但啥成就感也沒有,而且誰也不會覺得你牛13。然而,如果你發明瞭一種用兩根筷子也能打開啤酒瓶的辦法,或者你乾脆生就一口好牙可以把瓶蓋啃開,那也許就大不一樣了。人家就會覺得你很好很強大。

事實8:每個人都喜歡戴著腳鐐跳舞。

也就是說,如果你用一個天生為某個目的的工具來做他該做的事情,沒有人會喝彩,你也不會覺得了不起。但如果你用兩個本身不是為某個目的的工具組合出新功能的話,你就是“創新”者(儘管也許本來就有某個現成的工具可用)。

而C++則是這些“創新”的土壤,是的,我說的就是無窮無盡的workarounds和慣用法。但問題是,這些“創新”其實根本不是創新,你必須認識到的是,他們都只不過是在沒有first-class解決方案的前提下不得已折騰出來的替補方案。是的,它們某種程度上的確可以叫創新,甚至研究可行的解決方案本身也是一件非常有意思的事情,但——

事實9:我知道它們很有趣,但實際上它們只是補丁方案。

是的,不要因為這些“創新”方案有趣就忍不住一頭鑽進去。你之所以覺得有趣是因為當你一定程度上熟悉了C++之後,C++的所有一切,包括缺陷,對你來說就成了一個“既定事實”,一個背景,一個習以為常的東西(人是有很強的適應性的)。因此,當你發現在這個習以為常的環境下居然出現了新的可能性時,你當然是會歡呼雀躍的(比如我當年讀《Modern C++ Design》的時候就有一次從早讀到晚,午飯都沒吃),然而實際上呢?其它語言中也許早就有first-class的支持了,其它語言也許根本不需要這個慣用法,因為它們就沒有這些缺陷。此外,從實踐的角度來說,更重要的是,這些“解決方案”也許你平時編程根本就用不到。

不,我當然不是說這些補丁方案不重要。正如前面所說,C++中繁雜的技巧並非空穴來風,總有實際問題在背後驅動的。但問題是,對於我們日常編程來說,這些“實際問題”簡直是八杆子打不著的。犯不著先費上80%的勁兒把20%時候才用到的東西揣在腦子裡,用的時候查文檔或書就行了。

看到這裡,塑造C++中特定的心態哲學的另一個原因想必你也已經知道了。實際上,這個原因才是真正根本的。前面說的一個原因是C++書籍市場(教育)造就的,然而為什麼人們喜歡寫這些書呢?進一步說,為什麼人們喜歡讀這些書呢?(我承認,我也曾經讀得津津有味。)答案很簡單:心理。每個人都喜歡戴著腳鐐跳舞(事實8)。認識到這一點不是為了提倡它,而是隻有當我們認識到自己為什麼會津津有味地去鑽研一堆補丁解決方案的時候,我們才真正能夠擺脫它們的吸引。

總而言之,C++的複雜性只是一個必要條件,並非問題的根本癥結。根本癥結在於人的心理,每個人都喜歡戴著腳鐐跳舞,並且以為是“創新”。意識到這一點之後可以幫我們避免被各種各樣名目繁多的語言細節和技巧佔去不必要的時間。

然而,C++的複雜性始終是一個不可迴避的現實。C++中有大量的陷阱和缺陷,後者導致了數目驚人的慣用法和workarounds。不加選擇的全盤預先學習,是非常糟糕的做法,不僅低效,而且根本沒有必要,實在是浪費生命。愛因斯坦曾經說過,“我只想知道‘他’(宇宙)的設計理念,其它的都是細節”。然而,正如另一些讀者指出的,如果對C++中的這些細節事先一點都沒有概念的話,那麼實際編碼中一旦遇到恐怕就變成沒頭蒼蠅了,也許到哪裡去RTFM都不知道。這也是為什麼那麼多C++面試都會不厭其煩地問一些有代表性的語言細節的原因。

把細節全盤裝在腦子裡固然不好,但對細節一無所知同樣也不是個辦法。那麼對於C++程序員來說,在學習中究竟應該以怎樣的態度和學習方法來對付C++的複雜性呢?其實答案也非常簡單,首先有一些很重要&必須的語言細節&特性是需要掌握的,然後我們只需知道在C++中大抵有哪些地方有複雜性(陷阱、缺陷),那麼遇到問題的時候自然能夠知道到哪兒去尋找答案了。具體的建議在後文。

C++的複雜性分類

本來這一節是打算做成一個C++複雜性索引的,然而一來C++的複雜性太多,二來網上其實已經有許多資料(比如Bjarne Stroustrup本人的C++ Technical FAQ就是一個很好的文檔),加上市面上的大多數C++書裡面也不停的講語言細節;因此實際上我們不是缺乏資料,而是缺乏一種索引這些資料的辦法,以及一種掌控這些複雜性的模塊化思維方法。

由於以上原因,這裡並不詳細羅列C++的複雜性,而是提供一個分類標準。

C++的複雜性有兩種分類辦法,一是分為非本質複雜性和本質複雜性;其中非本質複雜性分為缺陷和陷阱兩類。另一種分類辦法是按照場景分類:庫開發場景下的複雜性和日常編碼的複雜性。從從事日常編碼的實踐者的角度來說,採用後一種分類可以讓我們迅速掌握80%場景下的複雜性。

二八法則

以下通過列舉一些常見的例子來解釋這種分類標準: 80%場景下的複雜性:

  1. 資源管理(C++日常複雜性的最主要來源):深拷貝&淺拷貝;類的四個特殊成員函數;使用STL;RAII慣用法;智能指針等等。
  2. 對象生命期:局部&全局對象生存期;臨時對象銷燬;對象構造&析構順序等等。
  3. 多態
  4. 重載決議
  5. 異常(除非你不用異常):棧開解(stack-unwinding)的過程;什麼時候拋出異常;在什麼抽象層面上拋出異常等等。
  6. undefined&unspecified&implementation defined三種行為的區別:i++ + ++i是undefined behavior(未定義行為——即“有問題的,壞的行為,理論上什麼事情都可能發生”);參數的求值順序是unspecified(未指定的——即“你不能依賴某個特定順序,但其行為是良好定義的”);當一個double轉換至一個float時,如果double變量的值不能精確表達在一個float中,那麼選取下一個接近的離散值還是上一個接近的離散值是implementation defined(實現定義的——即“你可以在實現商的編譯器文檔中找到說明”)。這些問題會影響到你編寫可移植的代碼。

(注:以上只是一個不完全列表,用於演示該分類標準的意義——實際上,如果我們只考慮“80%場景下的複雜性”,記憶和學習的負擔便會大大減小。) 20%場景下的複雜性:

  1. 對象內存佈局
  2. 模板:偏特化;非類型模板參數;模板參數推導規則;實例化;二段式名字查找;元編程等等。
  3. 名字查找&綁定規則
  4. 各種缺陷以及缺陷衍生的workarounds(C++書中把這些叫做“技術”):不支持concepts(boost.concept_check庫);類型透明的typedef(true-typedef慣用法);弱類型的枚舉(強枚舉慣用法);隱式bool轉換(safe-bool慣用法);自定義類型不支持初始化列表(boost.assign庫);孱弱的元編程支持(type-traits慣用法;tag-dispatch慣用法;boost.enable_if庫;boost.static_assert庫);右值缺陷(loki.mojo庫);不支持可變數目的模板參數列表(type-list慣用法);不支持native的alignment指定。

(注:以上只是一個不完全列表。你會發現,這些細節或技術在日常編程中極少用到,尤其是各種語言缺陷衍生出來的workarounds,構成了一個巨大的長尾,在無論是C++的書還是文獻中都佔有了很大的比重,作者們稱它們為技術,然而實際上這些“技術”絕大多數只在庫開發當中需要用到。)

非本質複雜性&本質複雜性

此外,考慮另一種分類辦法也是有幫助的,即分為非本質複雜性和本質複雜性。

非本質複雜性(不完全列表)

  1. 缺陷(指能夠克服的問題,但解決方案很笨拙;C++的書裡面把克服缺陷的workarounds稱作技術,我覺得非常誤導):例子在前面已經列了一堆了。
  2. 陷阱(指無法克服的問題,只能小心繞過;如果跌進去,那就意味著你不知道這個陷阱,那麼很大可能性你也不知道從哪去解決這個問題):一般來說,作為一個合格的程序員(不管是不是C++程序員),80%場景下的語言陷阱是需要記住才行的。比如深拷貝&淺拷貝;基類的析構函數應當為虛;缺省生成的類成員函數;求值順序&序列點;類成員初始化順序&聲明順序;導致不可移植代碼的實現相關問題等。

本質複雜性(不完全列表)

  1. 內存管理
  2. 對象生命期
  3. 重載決議
  4. 名字查找
  5. 模板參數推導規則
  6. 異常
  7. OO(動態)和GP(靜態)兩種範式的應用場景和交互

總而言之,這一節的目的是要告訴你從一個較高的層次去把握C++中的複雜性。其中最重要的一個指導思想就是在學習的過程中注意你正學習的技術或細節到底是80%場景下的還是20%場景下的(一般來說,讀完兩本書——後面會提到——之後你就能夠很容易的對此進行判斷了),如果是20%場景下的(有大量這類複雜性,其中尤數各種各樣的workarounds為巨),那麼也許最好的做法是隻記住一個大概,不去作任何深究。此外,一般來說,不管使用哪門語言,認識語言陷阱對於編程來說都是一個必要的條件,語言陷阱的特點是如果你掉進去了,那麼很大可能意味著你本來就不知道這有個陷阱,後者很大可能意味著你不知道如何解決。

學習C++:實踐者的方法

在上面寫了那麼多之後,如何學習C++這個問題的答案其實已經很明顯了。我們所欠缺的是一個書單。

第一本

如果你是一個C++程序員,那麼很大的可能性你會需要用到底層知識(硬件平臺架構、緩存、指令流水線、硬件優化、內存、整數&浮點數運算等);這是因為兩個主要原因:一,瞭解底層知識有助於寫出高效的代碼。二,C++這樣的接近硬件的語言為了降低語言抽象的效率懲罰,在語言設計上作了很多折衷,比如內建的有限精度整型和浮點型,比如指針。這就意味著,用這類語言編程容易掉進Joel所謂的“抽象漏洞”,需要你在語言提供的抽象層面之下去思考並解決遇到的問題,此時的底層知識便能幫上大忙。因此,一本從程序員(而不是電子工程師)的角度去介紹底層知識的書會非常有幫助——這就是推薦《Computer Systems:A Programmers Perspective》(以下簡稱CSAPP)(中譯本《深入理解計算機系統》)的原因。

第二本

C++是在C語言大行其道的歷史背景下發展起來的,在一開始以及後來的相當長一段時間內,C++是C的超集,所有C的特性在C++裡面都有,因此導致了大量後來的C++入門書籍都從C講起,實際上,這是一個誤導,因為C++雖然是C的超集,然而用抽象機制擴展C語言的重大意義就在於用抽象去覆蓋C當中裸露的種種語言特性,讓程序員能夠在一個更自然的抽象層面上編程,比如你不是用int*加一個數組大小n來表示一個數組,而是用可自動增長的vector;比如你不是用malloc/free,而是用智能指針和RAII技術來管理資源;比如你不是用一個只包含數據的結構體加上一組函數來做一個暴露的類,而是使用真正的ADT。比如你不是使用second-class的返回值來表達錯誤,而是利用first-class的語言級異常機制等等。然而,C畢竟是C++的源頭,剝開C++的抽象外衣,底層仍然還是C;而且,更關鍵的是,在實際編碼當中,有時候還的確要“C”一把,比如在模塊級的二進制接口封裝上。Bjarne也說過,OO/GP這些抽象機制只有用在合適的地方才是合適的。當人們手頭有的是錘子的時候,很容易把所有的目標都當成釘子,有時候C的確能夠提供簡潔高效的解決方案,比如C標準庫裡面的printf和fopen(此例受雲風的啟發)的使用界面就是典型的例子。簡而言之,理解C語言的精神不僅有助於更好地理解C++,更理性地使用C++,而且也有其實踐意義——這就是推薦《The C Programming Language》(以下簡稱TCPL)的原因。此外,建議在閱讀《Accelerated C++》之前先閱讀《The C Programming Language》。因為,一,《The C Programming Language》非常薄。二,如果你帶著比較的眼光去看問題,看完《The C Programming Language》再看《Accelerated C++》,你便會更深刻的理解C++語言引入抽象機制的意義和實際作用。

第三本(是的,第三本)

另一方面,C++不同於C的一個關鍵地方就在於,C++在完全保留有C的高效的基礎上,增添了抽象機制。而所謂的“現代C++風格”便是倡導正確利用C++的抽象機制和這些機制構建出來的現代C++庫(以STL為代表)的,Bjarne也很早就倡導將C++當作一門不同於C的新語言來學習(就拿內存管理來說,使用現代C++的內存管理技術,幾乎可以完全避免new和delete),因此,一本從這個思路來介紹C++的入門書籍是非常必要的——這就是推薦《Accelerated C++》的原因(以下簡稱AC++)。《Accelerated C++》的作者Andrew Koenig是C++標準化過程中的核心人物之一。

第四本

《Accelerated C++》固然寫得非常漂亮,但正如所有漂亮的入門書一樣,它的優點和弱點都在於它的輕薄短小。短短3百頁,對現代C++的運用精神作了極好的概述。然而要熟練運用C++,我們還需要更多的講解,這個時候一本全面但又不鑽語言牛角尖,從“語言是如何支持抽象設計”的角度而不是“為了講語言特性而講語言特性”的角度來介紹一門語言的書便至關重要,在C++裡面,我還沒有見到比C++之父本人的《The C++ Programming Language》(以下簡稱TC++PL)做得更好的,C++之父本人既有大規模C++運用的經驗又有語言設計思想的最本質把握,因此TC++PL才能做到高屋建瓴,不為細節所累;同時又能做到實踐導向,不落於為介紹語言而介紹語言的巢臼。最後有一個需要提醒的地方,TC++PL其實沒有它看起來那麼厚,因為真正介紹語言的內容只有區區500頁(第一部分:基礎;第二部分:抽象機制;以及第四部分:用C++設計),剩下的是介紹標準庫的,可以當作Manual(參考手冊)。

建議3:CSAPP &TCPL& AC++&TC++PL。

是的,在C++方面登堂入室並不需要閱讀多得恐怖的所謂“經典”,至於為什麼這些“經典”無需閱讀,前面已經講的很詳細了。其實你只需要這四本書,就可以奠定一個深厚的基礎,以及對C++的成熟理性的現代運用理念。其餘的書都可以當成參考資料,用到的時候再去翻閱,即:

建議4:實踐驅動地學習。

實踐驅動當然不代表什麼基礎都不打,直接捋起袖管就上。不管運用哪種工具,首先都需要知道關於它的一定程度的基本知識(包括應該怎麼用,和不應該怎麼用)。知道應該怎麼用可以幫你發揮出它的正確和最大效用,知道不應該怎麼用則可以幫你避免用的過程中傷及自身的危險。這就是為什麼我建議你看四本書,以及建議你要了解C++中的陷阱(大部分來自C,因此你可以閱讀《C缺陷和陷阱》)的原因。

實踐驅動代表著一旦一個紮實的基礎具備了之後獲得延伸知識的方式。出於環境和心理的原因,C++學習者們在這條路上走錯的機率非常大,許多人乃至以上來就拿Effective C++&More Effective C++、Inside C++ Object Model這類書去讀(是的,我也是,所以我才會在這裡寫下這篇文章),結果讀了一本又一本,出現知道虛函數實現機制的每個細節卻不知道虛函數作用的情況。

實踐驅動其實很簡單:實踐+查文檔。知識便在這樣一個簡單的循環中積累起來。實踐驅動的最大好處就是你學到的都是實踐當中真正需要的,屬於那“80%”最有用的。而查文檔的重要性前面已經說過了,但對於C++實踐者來說,哪些“文檔”是非常重要的呢?

第一本《The Pragmatic Programmer》

用本程序員的傑作;雖然不是一本C++的書,但其介紹的實踐理念卻是所有程序員都需要的。

第二本《C++ Coding Standard》

無需多作介紹,這是一本濃縮了C++社群多年來寶貴的經驗結晶的書,貼近實踐,處處以80%場景為主導,不鑽語言旮旯,用本為主…總之,非常值得放在手邊時時參閱。因為書很薄,所以也不妨先往腦袋裡面裝一遍。書中的101條建議的介紹都很簡略,並且指出了詳細介紹的延伸閱讀,在延伸閱讀的時候還是要注意不要陷入無關的細節和不必要的技巧中,時時抬頭看一看你需要解決的問題。在C++編碼標準方面,Bjarne也有一些建議。

第三本《Code Complete, 2nd Edition》

這是一本非常卓越的參考資料,涉及開發過程的全景,有大量寶貴的經驗。你未必要一口氣讀完,但你至少應該知道它裡面都寫了哪些內容,以便可以回頭參閱。

其它

所有優秀的技術書籍都是資料來源。一旦養成了查文檔的習慣,所有的電子書、紙書、網絡上的資源實際上都是你的財富。不過,查文檔的前提是你要從手邊的問題分析出應該到什麼地方去查資料,這裡,分析問題的能力很重要,因此:

建議5:思考。

這個建議就把我們帶到了第四本書

第四本:《你的燈亮著嗎?》

不作介紹,自己閱讀,這本書只有一百多頁,但精彩非常,妙趣橫生。 最後,要想理性地運用一門語言,不僅需要看到這門語言的特點,還要能夠從另一個角度去看這門語言——即看到它的缺點,因為從心理上——

事實10:一旦我們熟悉了一門語言之後,就容易不知不覺地在其框架下思考,受到語言特性的細節的影響,作出second-class的設計。

對於像C++這樣的在抽象機制上作了折衷的語言,尤其如此,思考容易受到語言機制本身細節的影響,往往在心裡頭還沒想好怎麼抽象,就已經確定了使用什麼語言機制乃至技巧;更有甚者是為了使用某個特性而去使用某個特性。然而,實際上,我們應該——

建議6:脫離語言思考,使用語言實現。

關於設計的一般理念,Eric Raymond在《The Art of Unix Programming》的第二部分有非常精彩的闡述。 此外,除了脫離語言的具體抽象機制來思考設計之外,學習其它語言對同類抽象機制的支持也是非常有益的,正如老話所說,“兼聽則明”。前一陣子reddit上也常出現“How Learning XXX help me become a Better YYY programmer”(其中XXX和YYY指代編程語言)的帖子,正是這個道理,這就把我們帶到了最後一個建議:學習其它語言。

建議7:學習其它語言。

如果你是一個系統程序員,你可能會覺得沒有必要學習其它語言,然而未必如此,你未必需要精通其它語言,而是可以去試著瞭解其它語言的設計理念,是如何支持日常編程中的設計的。這一招非常有利於在使用你自己的語言編程時心理上脫離語言機制細節的影響,作出更好的抽象設計。 尾聲

建議8(可選):重讀本文。

注:這篇文章的目的是給國內的C++學習者(尤其是初學者)一個可操作的建議。我打算不斷修訂並完善它;因為這是根據我個人的經驗來寫的,而基於我對C++的熟悉程度,可能會有地方並不能完完全全站到初學者的視角來看問題。我估計會有這樣的地方,所以,如果有任何建議,請發郵件給我:pongba@gmail.com