zhuan zhai學習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++庫(以STL爲代表) 的,Bjarne也很早就倡導將C++當作一門不同於C的新語言來學習(就拿內存管理來說,使用現代C++的內存管理技術,幾乎可以完全避免new和 delete),因此,一本從這個思路來介紹C++的入門書籍是非常必要的——這就是推薦《Accelerated C++》的原因(以下簡稱AC++)。《Accelerated C++》的作者Andrew Koenig是C++標準化過程中的核心人物之一。

第二本

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++語言引入抽象機制的意義和實際作用。

第四本

《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++實踐者來說,哪些“文檔”是非常重要的呢?

第二本

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

第一本

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

第三本

《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++的熟悉程度,可能會有地方並不能完完全全站到初學者的視角來看問題。我估 計會有這樣的地方,所以,如果有任何建議,請發郵件給我: [email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章