1. 程式人生 > >從《C++ Primer 第四版》入手學習 C++

從《C++ Primer 第四版》入手學習 C++

為什麼要學習C++?

2009 年本書作者 Stan Lippman 先生來華參加上海祝成科技舉辦的C++技術大會,他表示人們現在還用C++的惟一理由是其效能。相比之下,Java/C#/Python等語言更加易學易用並且開發工具豐富,它們的開發效率都高於C++。但C++目前仍然是執行最快的語言[1],如果你的應用領域確實在乎這個效能,那麼 C++ 是不二之選。

這裡略舉幾個例子[2]。對於手持裝置而言,提高執行效率意味著完成相同的任務需要更少的電能,從而延長裝置的操作時間,增強使用者體驗。對於嵌入式[3]裝置而言,提高執行效率意味著:實現相同的功能可以選用較低檔的處理器和較少的儲存器,降低單個裝置的成本;如果裝置銷量大到一定的規模,可以彌補C++開發的成本。對於分散式系統而言,提高10%的效能就意味著節約10%的機器和能源。如果系統大到一定的規模(數千臺伺服器),值得用程式設計師的時間去換取機器的時間和數量,可以降低總體成本。另外,對於某些延遲敏感的應用(遊戲[4]

,金融交易),通常不能容忍垃圾收集(GC)帶來的不確定延時,而C++可以自動並精確地控制物件銷燬和記憶體釋放時機[5]。我曾經不止一次見到,出於效能原因,用C++重寫現有的Java或C#程式。

C++之父Bjarne Stroustrup把C++定位於偏重系統程式設計(system programming) [6]的通用程式設計語言,開發資訊基礎架構(infrastructure)是C++的重要用途之一[7]。Herb Sutter總結道[8],C++注重執行效率(efficiency)、靈活性(flexibility)[9]和抽象能力(abstraction),併為此付出了生產力(productivity)方面的代價[10]

。用本書作者的話來說,C++ is about efficient programming with abstractions。C++的核心價值在於能寫出“執行效率不打折扣的抽象[11]”。

要想發揮C++的效能優勢,程式設計師需要對語言本身及各種操作的代價有深入的瞭解[12],特別要避免不必要的物件建立[13]。例如下面這個函式如果漏寫了&,功能還是正確的,但效能將會大打折扣。編譯器和單元測試都無法幫我們查出此類錯誤,程式設計師自己在編碼時須得小心在意。

inline int find_longest(const std::vector<std::string>& words)
{
  // std::max_element(words.begin(), words.end(), LengthCompare());
}

在現代CPU體系結構下,C++ 的效能優勢很大程度上得益於對記憶體佈局(memory layout )的精確控制,從而優化記憶體訪問的區域性性[14](locality of reference)並充分利用記憶體階層(memory hierarchy)提速[15],這一點優勢在近期內不會被基於GC的語言趕上[16]

C++的協作性不如C、Java、Python,開源專案也比這幾個語言少得多,因此在TIOBE語言流行榜中節節下滑。但是據我所知,很多企業內部使用C++來構建自己的分散式系統基礎架構,並且有替換Java開源實現的趨勢。

學習C++只需要讀一本大部頭

C++不是特性(features)最豐富的語言,卻是最複雜的語言,諸多語言特性相互干擾,使其複雜度成倍增加。鑑於其學習難度和知識點之間的關聯性,恐怕不能用“粗粗看看語法,就擼起袖子開幹,邊查Google邊學習[17]”這種方式來學習C++,那樣很容易掉到陷阱裡或養成壞的程式設計習慣。如果想成為專業C++開發者,全面而深入地瞭解這門複雜語言及其標準庫,你需要一本系統而權威的書,這樣的書必定會是一本八九百頁的大部頭[18]

兼具系統性和權威性[19]的C++教材有兩本,C++之父Bjarne Stroustrup的代表作《The C++ Programming Language》和Stan Lippman的這本《C++ Primer》。侯捷先生評價道:“泰山北斗已現,又何必案牘勞形於墨瀚書海之中!這兩本書都從C++盤古開天以來,一路改版,斬將擎旗,追奔逐北,成就一生榮光[20]。”

從實用的角度,這兩本書讀一本即可,因為它們覆蓋的C++知識點相差無幾。就我個人的閱讀體驗而言,Primer更易讀一些,我十年前深入學習C++正是用的《C++ Primer第三版》。這次借評註的機會仔細閱讀了《C++ Primer第四版》,感覺像在讀一本完全不同的新書。第四版內容組織及文字表達比第三版進步很多[21],第三版可謂“事無鉅細、面面俱到”,第四版重點突出詳略得當,甚至篇幅也縮短了,這多半歸功於新加盟的作者Barbara Moo。

《C++ Primer 第四版》講什麼?適合誰讀?

這是一本C++語言的教程,不是程式設計教程。本書不講八皇后問題、Huffman編碼、漢諾塔、約瑟夫環、大整數運算等等經典程式設計例題,本書的例子和習題往往都跟C++本身直接相關。本書的主要內容是精解C++語法(syntax)與語意(semantics),並介紹C++標準庫的大部分內容(含STL)。“這本書在全世界C++教學領域的突出和重要,已經無須我再贅言[22]。”

本書適合C++語言的初學者,但不適合程式設計初學者。換言之,這本書可以是你的第一本C++ 書,但恐怕不能作為第一本程式設計書。如果你不知道什麼是變數、賦值、分支、條件、迴圈、函式,你需要一本更加初級的書[23],本書第1章可用作自測題。

如果你已經學過一門程式語言,並且打算成為專業C++開發者,從《C++ Primer 第四版》入手不會讓你走彎路。值得特別說明的是,學習本書不需要事先具備C語言知識。相反,這本書教你編寫真正的C++程式,而不是披著C++ 外衣的C程式。

《C++ Primer 第四版》的定位是語言教材,不是語言規格書,它並沒有面面俱到地談到C++的每一個角落,而是重點講解C++程式設計師日常工作中真正有用的、必須掌握的語言設施和標準庫[24]。本書的作者一點也不炫耀自己的知識和技巧,雖然他們有十足的資本[25]。這本書用語非常嚴謹(沒有那些似是而非的比喻),用詞平和,講解細緻,讀起來並不枯燥。特別是如果你已經有一定的程式設計經驗,在閱讀時不妨思考如何用C++來更好地完成以往的程式設計任務。

儘管本書篇幅近900頁,其內容還是十分緊湊,很多地方讀一個句子就值得寫一小段程式碼去驗證。為了節省篇幅,本書經常修改前文程式碼中的一兩行,來說明新的知識點,值得把每一行程式碼敲到機器中去驗證。習題當然也不能輕易放過。

《C++ Primer 第四版》體現了現代C++教學與程式設計理念:在現成的高質量類庫上構建自己的程式,而不是什麼都從頭自己寫。這本書在第三章介紹了string和vector這兩個常用的類,立刻就能寫出很多有用的程式。但作者不是一次性把string的上百個成員函式一一列舉,而是有選擇地講解了最常用的那幾個函式。

《C++ Primer 第四版》的程式碼示例質量很高,不是那種隨手寫的玩具程式碼。第10.4.2節實現了帶禁用詞的單詞計數,第10.6利用標準庫容器簡潔地實現了基於倒排索引思路的文字檢索,第15.9節又用面向物件方法擴充了文字檢索的功能,支援布林查詢。值得一提的是,這本書講解繼承和多型時舉的例子符合Liskov替換原則,是正宗的面向物件。相反,某些教材以複用基類程式碼為目的,常以“人、學生、老師、教授”或“僱員、經理、銷售、合同工”為例,這是誤用了面向物件的“複用”。

《C++ Primer 第四版》出版於2005年,遵循2003年的C++語言標準[26]。C++新標準已於2011年定案(稱為C++11),本書不涉及TR1[27]和C++11,這並不意味著這本書過時了[28]。相反,這本書裡沉澱的都是當前廣泛使用的C++程式設計實踐,學習它可謂正當時。評註版也不會越俎代庖地介紹這些新內容,但是會指出哪些語言設施已在新標準中廢棄,避免讀者浪費精力。

《C++ Primer 第四版》是平臺中立的,並不針對特定的編譯器或作業系統。目前最主流的C++編譯器有兩個, GNU G++和微軟Visual C++。實際上,這兩個編譯器陣營基本上“模塑[29]”了C++語言的行為。理論上講, C++語言的行為是由C++標準規定的。但是 C++不像其他很多語言有“官方參考實現[30]”,因此C++的行為實際上是由語言標準、幾大主流編譯器、現有不計其數的C++產品程式碼共同確定的,三者相互制約。C++編譯器不光要儘可能符合標準,同時也要遵循目標平臺的成文或不成文規範和約定,例如高效地利用硬體資源、相容作業系統提供的C語言介面等等。在C++標準沒有明文規定的地方,C++編譯器也不能隨心所欲自由發揮。學習C++的要點之一是明白哪些行為是由標準保證的,哪些是由實現(軟硬體平臺和編譯器)保證的[31],哪些是編譯器自由實現,沒有保證的;換言之,明白哪些程式行為是可依賴的。從學習的角度,我建議如果有條件不妨兩個編譯器都用[32],相互比照,避免把編譯器和平臺特定的行為誤解為C++語言規定的行為。儘管不是每個人都需要寫跨平臺的程式碼,但也大可不必自我限定在編譯器的某個特定版本,畢竟編譯器是會升級的。

本著“練從難處練,用從易處用”的精神,我建議在命令列下編譯執行本書的示例程式碼,並儘量少用偵錯程式。另外,值得了解C++的編譯連結模型[33],這樣才能不被實際開發中遇到的編譯錯誤或連結錯誤絆住手腳。(C++不像現代語言那樣有完善的模組(module)和包(package)設施,它從C語言繼承了標頭檔案、原始檔、庫檔案等古老的模組化機制,這套機制相對較為脆弱,需要花一定時間學習規範的做法,避免誤用。)

就學習C++語言本身而言,我認為有幾個練習非常值得一做。這不是“重複發明輪子”,而是必要的程式設計練習,幫助你熟悉掌握這門語言。是寫一個複數類或者大整數類[34],實現基本的運算,熟悉封裝與資料抽象。是寫一個字串類,熟悉記憶體管理與拷貝控制。是寫一個簡化的vector<T>類模板,熟悉基本的模板程式設計,你的這個vector應該能放入int和string等元素型別。是寫一個表示式計算器,實現一個節點類的繼承體系(右圖),體會面向物件程式設計。前三個練習是寫獨立的值語義的類,第四個練習是物件語義,同時要考慮類與類之間的關係。

表示式計算器能把四則運算式3+2*4解析為左圖的表示式樹[35],對根節點呼叫calculate()虛擬函式就能算出表示式的值。做完之後還可以再擴充功能,比如支援三角函式和變數。


在寫完面向物件版的表示式樹之後,還可以略微嘗試泛型程式設計。比如把類的繼承體系簡化為下圖,然後用BinaryNode<std::plus<double> >和BinaryNode<std:: multiplies<double> >來具現化BinaryNode<T>類模板,通過控制模板引數的型別來實現不同的運算。


在表示式樹這個例子中,節點物件是動態建立的,值得思考:如何才能安全地、不重不漏地釋放記憶體。本書第15.8節的Handle可供參考。(C++的面向物件基礎設施相對於現代的語言而言顯得很簡陋,現在C++也不再以“支援面向物件”為賣點了。)

C++難學嗎?“能夠靠讀書看文章讀程式碼做練習學會的東西沒什麼門檻,智力正常的人只要願意花功夫,都不難達到(不錯)的程度。[36]” C++好書很多,不過優秀的C++開原始碼很少,而且風格迥異[37]。我這裡按個人口味和經驗列幾個供讀者參考閱讀:Google的protobuf、leveldb、PCRE的C++ 封裝,我自己寫的muduo網路庫。這些程式碼都不長,功能明確,閱讀難度不大。如果有時間,還可以讀一讀Chromium中的基礎庫原始碼。在讀Google開源的C++程式碼時要連註釋一起細讀。我不建議一開始就讀STL或Boost的原始碼,因為編寫通用C++模板庫和編寫C++應用程式的知識體系相差很大。 另外可以考慮讀一些優秀的C或Java開源專案,並思考是否可以用C++更好地實現或封裝之(特別是資源管理方面能否避免手動清理)。

繼續前進

我能夠隨手列出十幾本C++好書,但是從實用角度出發,這裡只舉兩三本必讀的書。讀過《C++ Primer》和這幾本書之後,想必讀者已能自行識別C++圖書的優劣,可以根據專案需要加以鑽研。

第一本是《Effective C++ 第三版》[38]。學習語法是一回事,高效地運用這門語言是另一回事。C++是一個遍佈陷阱的語言,吸取專家經驗尤為重要,既能快速提高眼界,又能避免重蹈覆轍。《C++ Primer》加上這本書包含的C++知識足以應付日常應用程式開發。

我假定讀者一定會閱讀這本書,因此在評註中不引用《Effective C++ 第三版》的任何章節。

《Effective C++ 第三版》的內容也反映了C++用法的進步。第二版建議“總是讓基類擁有虛解構函式”,第三版改為“為多型基類宣告虛解構函式”。因為在C++中,“繼承”不光只有面向物件這一種用途,即C++的繼承不一定是為了覆寫(override)基類的虛擬函式。第二版花了很多筆墨介紹淺拷貝與深拷貝,以及對指標成員變數的處理[39]。第三版則提議,對於多數class而言,要麼直接禁用拷貝建構函式和賦值操作符,要麼通過選用合適的成員變數型別[40],使得編譯器預設生成的這兩個成員函式就能正常工作。

什麼是C++程式設計中最重要的程式設計技法(idiom)?我認為是“用物件來管理資源”,即RAII。資源包括動態分配的記憶體[41],也包括開啟的檔案、TCP網路連線、資料庫連線、互斥鎖等等。藉助RAII,我們可以把資源管理和物件生命期管理等同起來,而物件生命期管理在現代C++里根本不是困難(見注5),只需要花幾天時間熟悉幾個智慧指標[42]的基本用法即可。學會了這三招兩式,現代的C++程式中可以完全不寫delete,也不必為指標或記憶體錯誤操心。現代C++程式裡出現資源和記憶體洩漏的惟一可能是迴圈引用,一旦發現,也很容易修正設計和程式碼。這方面的詳細內容請參考《Effective C++ 第三版》第3章資源管理。

C++是目前惟一能實現自動化資源管理的語言,C語言完全靠手工釋放資源,而其他基於垃圾收集的語言只能自動清理記憶體,而不能自動清理其他資源[43](網路連線,資料庫連線等等)。

除了智慧指標,TR1中的bind/function也十分值得投入精力去學一學[44]。讓你從一個嶄新的視角,重新審視類與類之間的關係。Stephan T. Lavavej有一套PPT介紹TR1的這幾個主要部件[45]

第二本書,如果讀者還是在校學生,已經學過資料結構課程[46],可以考慮讀一讀《泛型程式設計與STL》[47];如果已經工作,學完《C++ Primer》立刻就要參加C++專案開發,那麼我推薦閱讀《C++程式設計規範》[48]

泛型程式設計有一套自己的術語,如concept、model、refinement等等,理解這套術語才能閱讀泛型程式庫的文件。即便不掌握泛型程式設計作為一種程式設計方法,也要掌握C++中以泛型思維設計出來的標準容器庫和演算法庫(STL)。坊間面向物件的書琳琅滿目,學習機會也很多,而泛型程式設計只有這麼一本,讀之可以開拓視野,並且加深對STL的理解(特別是迭代器[49])和應用。

C++模板是一種強大的抽象手段,我不贊同每個人都把精力花在鑽研艱深的模板語法和技巧。從實用角度,能在應用程式中寫寫簡單的函式模板和類模板即可(以type traits為限),不是每個人都要去寫公用的模板庫。

由於C++語言過於龐大複雜,我見過的開發團隊都對其剪裁使用[50]。往往團隊越大,專案成立時間越早,剪裁得越厲害,也越接近C。制定一份好的程式設計規範相當不容易。規範定得太緊(比如定為團隊成員知識能力的交集),程式設計師束手束腳,限制了生產力,對程式設計師個人發展也不利[51]。規範定得太鬆(定為團隊成員知識能力的並集),專案內程式碼風格迥異,學習交流協作成本上升,恐怕對生產力也不利。由兩位頂級專家合寫的《C++程式設計規範》一書可謂是現代C++程式設計規範的範本。

《C++程式設計規範》同時也是專家經驗一類的書,這本書篇幅比《Effective C++ 第三版》短小,條款數目卻多了近一倍,可謂言簡意賅。有的條款看了就明白,照做即可:

·         第1條,以高警告級別編譯程式碼,確保編譯器無警告。

·         第31條,避免寫出依賴於函式實參求值順序的程式碼。C++操作符的優先順序、結合性與表示式的求值順序是無關的。裘宗燕老師寫的《C/C++ 語言中表達式的求值》[52]一文對此有明確的說明。

·         第35條,避免繼承“並非設計作為基類使用”的class。

·         第43條,明智地使用pimpl。這是編寫C++動態連結庫的必備手法,可以最大限度地提高二進位制相容性。

·         第56條,儘量提供不會失敗的swap()函式。有了swap()函式,我們在自定義賦值操作符時就不必檢查自賦值了。

·         第59條,不要在標頭檔案中或#include之前寫using。

·         第73條,以by value方式丟擲異常,以by reference方式捕捉異常。

·         第76條,優先考慮vector,其次再選擇適當的容器。

·         第79條,容器內只可存放value和smart pointer。

有的條款則需要相當的設計與編碼經驗才能解其中三昧:

·         第5條,為每個物體(entity)分配一個內聚任務。

·         第6條,正確性、簡單性、清晰性居首。

·         第8、9條,不要過早優化;不要過早劣化。

·         第22條,將依賴關係最小化。避免迴圈依賴。

·         第32條,搞清楚你寫的是哪一種class。明白value class、base class、trait class、policy class、exception class各有其作用,寫法也不盡相同。

·         第33條,儘可能寫小型class,避免寫出大怪獸。

·         第37條,public繼承意味著可替換性。繼承非為複用,乃為被複用。

·         第57條,將class型別及其非成員函式介面放入同一個namespace。

值得一提的是,《C++程式設計規範》是出發點,但不是一份終極規範。例如Google的C++程式設計規範[53]和LLVM程式設計規範[54]都明確禁用異常,這跟這本書的推薦做法正好相反。

評註版使用說明

評註版採用大開本印刷,在保留原書板式的前提下,對原書進行了重新分頁,評註的文字與正文左右分欄並列排版。本書已依據原書2010年第11次印刷的版本進行了全面修訂。為了節省篇幅,原書每章末尾的小結和術語表還有書末的索引都沒有印在評註版中,而是做成PDF供讀者下載,這也方便讀者檢索。評註的目的是幫助初次學習C++的讀者快速深入掌握這門語言的核心知識,澄清一些概念、比較與其他語言的不同、補充實踐中的注意事項等等。評註的內容約佔全書篇幅的15%,大致比例是三分評、七分注,並有一些補白的內容[55]。如果讀者拿不定主意是否購買,可以先翻一翻第5章。我在評註中不談C++11[56],但會略微涉及TR1,因為TR1已經投入實用。

為了不打斷讀者閱讀的思路,評註中不會給URL連結,評註中偶爾會引用《C++程式設計規範》的條款,以[CCS]標明,這些條款的標題已在前文列出。另外評註中出現的soXXXXXX表示http://stackoverflow.com/questions/XXXXXX 網址。

網上資源

程式碼下載:http://www.informit.com/store/product.aspx?isbn=0201721481
豆瓣頁面:http://book.douban.com/subject/10944985/
術語表與索引PDF下載:http://chenshuo.com/cp4/
本文電子版釋出於https://github.com/chenshuo/documents/downloads/LearnCpp.pdf,方便讀者訪問腳註中的網站。

我的聯絡方式:giantchen_AT_gmail.com                    http://weibo.com/giantchen

陳碩

2012年3月

中國·香港

評註者簡介 :

陳碩,北京師範大學碩士,擅長 C++ 多執行緒網路程式設計和實時分散式系統架構。現任職於香港某跨國金融公司 IT 部門,從事實時外匯交易系統開發。編寫了開源 C++ 網路庫 muduo; 參與翻譯了《程式碼大全(第二版)》和《C++ 程式設計規範(繁體版)》;2009 年在上海 C++ 技術大會做技術演講《當解構函式遇到多執行緒》,同時擔任 Stanley Lippman 先生的口譯員;2010 年在珠三角技術沙龍做技術演講《分散式系統的工程化開發方法》;2012年在“我們的開源專案”深圳站做《Muduo 網路庫:現代非阻塞C++網路程式設計》演講。