?? 學習c++實踐者的方法.txt
字號:
如果書中介紹的某塊內容你認為在日常編程中基本不會用到(屬于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. 重載決議
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -