王海鵬
大約在1993年的時(shí)候,我開始學(xué)習(xí)C++。C++是我的“初戀情人”。雖然之前也學(xué)過Basic和Fortran,但只是作為學(xué)校里的一門課程來(lái)學(xué)的;而C++則是伴我度過青澀成長(zhǎng)歲月的編程語(yǔ)言。那時(shí)候國(guó)內(nèi)C++的書籍還非常少,對(duì)我影響最大的是《Borland C++ 3.0程序員指南》。近年來(lái)一直在用Java,算算時(shí)間,沒有關(guān)心C++也有五六年了,期間C++出了新標(biāo)準(zhǔn),我也是知之不詳。前些日子朋友送來(lái)一本《C++編程思想》(第2卷),正好趁此機(jī)會(huì)重學(xué)一遍。
這本書的第一部分主題是構(gòu)建穩(wěn)定的系統(tǒng)。初級(jí)程序員往往只考慮功能需求,而高級(jí)程序員將考慮更多的東西,包括健壯性、性能、可伸縮性、可維護(hù)性、可擴(kuò)展性等。構(gòu)建穩(wěn)定的系統(tǒng)就是功能之外首先值得考慮的東西。
作為一名Java程序員,我已經(jīng)能熟練使用,深知在軟件開發(fā)中,對(duì)發(fā)生概率較小的異常情況的處理可能會(huì)占掉相當(dāng)多的思考時(shí)間。而這反映在健壯軟件的代碼中,就是有相當(dāng)多的異常處理代碼。只是當(dāng)年學(xué)習(xí)使用C++時(shí),對(duì)軟件開發(fā)的理解還比較淺,對(duì)異常的處理接觸得相對(duì)也比較少。在C++中,基本數(shù)據(jù)類型(如int)也可以拋出,不像Java,只能拋出實(shí)現(xiàn)了Throwable接口的對(duì)象。在C++的try…catch語(yǔ)句中,特別要注意資源釋放的問題,Java亦是如此。
auto_ptr是為簡(jiǎn)化C++資源(特別是內(nèi)存)管理問題而提供的一種機(jī)制,書中通過一個(gè)簡(jiǎn)化的版本說(shuō)明了auto_ptr的原理。這里用到了模板。模板實(shí)現(xiàn)了另一種抽象層面的復(fù)用,有時(shí)候它的作用類似于宏。具體到“資源獲得式初始化(RAII)”,如果擁有資源的對(duì)象在超出其作用域時(shí)都需要釋放其資源,那么何不把這種行為集中實(shí)現(xiàn)?函數(shù)模板和通用算法也是如此。這樣做的結(jié)果是消除了重復(fù),減少了代碼量。這就是auto_ptr的設(shè)計(jì)動(dòng)機(jī)。
不像Java,C++沒有垃圾回收機(jī)制,所以C++程序員必須投入更多的精力來(lái)思考資源分配/釋放問題。如果某件事(如釋放內(nèi)存)總是被忘記,那就設(shè)計(jì)一種機(jī)制來(lái)防止再次忘記。總結(jié)起來(lái),在C++中使用對(duì)象或分配內(nèi)存,有以下兩個(gè)原則:(1)盡量在棧中分配對(duì)象;(2)如果要使用堆,就使用auto_ptr模板類。
要理解auto_ptr,需要程序員理解棧和堆、動(dòng)態(tài)內(nèi)存分配、棧反解、構(gòu)造函數(shù)和析構(gòu)函數(shù)的調(diào)、模板等概念。雖然也可以死記住這種用法,但說(shuō)不準(zhǔn)哪天就會(huì)帶來(lái)大麻煩。例如Java雖然有GC,但OutOFMemory的異常還是屢見不鮮。這就是知其然而不知其所以然的問題。C++對(duì)程序員提出了較高的要求,對(duì)PC結(jié)構(gòu)和內(nèi)存管理,結(jié)構(gòu)化編程和面向?qū)ο缶幊潭家邢喈?dāng)?shù)睦斫?。所以網(wǎng)上有人說(shuō):“真正的程序員使用C++?!?/p>
書中提到,雖然可以在方法聲明中聲明該方法可能會(huì)拋出的異常,但是標(biāo)準(zhǔn)C++庫(kù)中的函數(shù)卻都沒有這樣做。原因是當(dāng)模板與具體類綁定時(shí),異常類型可能是未知的。作者通過這一點(diǎn)告訴我們,雖然語(yǔ)言提供了某種機(jī)制,用還是不用,完全取決于使用者。
要構(gòu)建健壯的系統(tǒng),就不能不提防御式編程。這個(gè)概念與契約式編程有關(guān),強(qiáng)調(diào)程序單元有清晰的規(guī)格說(shuō)明,并在錯(cuò)誤發(fā)生的第一地點(diǎn)發(fā)現(xiàn)它,避免因?yàn)椤袄M(jìn),垃圾出”而引發(fā)不可知的嚴(yán)重后果。Meyer的“面向?qū)ο筌浖?gòu)造”對(duì)此有深入論述,這是嚴(yán)謹(jǐn)?shù)腛O程序員必須學(xué)習(xí)和理解的內(nèi)容。有人把契約式編程理解為一種責(zé)任劃定,出了問題的時(shí)候可以確定到底是哪個(gè)程序員的責(zé)任,所以可以假定別人都實(shí)現(xiàn)了契約,而不必再寫代碼進(jìn)行檢查。筆者認(rèn)為這樣的理解是有偏差的。難道與人簽訂了合同之后就不用檢查合同履行的情況?契約式編程的目的是為了得到更健壯的系統(tǒng),所以直接導(dǎo)致了防御式編程。
單元測(cè)試用于檢查一個(gè)程序單元對(duì)它的契約(規(guī)格說(shuō)明)執(zhí)行的情況。大致可以分為兩部分工作:(1)如果調(diào)用者不能滿足前置條件,程序是否能正確反應(yīng);(2)在調(diào)用者滿足前置條件的情況下,程序是否確保了后置條件的成立,并給出了預(yù)期的結(jié)果。讓設(shè)計(jì)變成可測(cè)試的規(guī)格說(shuō)明,讓測(cè)試可以自動(dòng)進(jìn)行。書中第2章還介紹了一個(gè)極為精簡(jiǎn)的測(cè)試框架,只包含兩個(gè)類。用最少的代碼來(lái)實(shí)現(xiàn)你的意圖,體現(xiàn)了簡(jiǎn)約之美。少即是多。如果讀者對(duì)單元測(cè)試想了解更多,可以去看Kent Beck的《測(cè)試驅(qū)動(dòng)開發(fā)》和其他一些書籍。熟悉JUnit的讀者則可以體會(huì)一下,用不同的語(yǔ)言來(lái)表述同樣的思想時(shí)的差異。
書中利用宏實(shí)現(xiàn)了調(diào)試時(shí)的代碼跟蹤。宏的運(yùn)用對(duì)于C程序員應(yīng)該不陌生,Java程序員則需要多花一些時(shí)間來(lái)體會(huì)。
調(diào)試和內(nèi)存泄漏檢查,都是C++程序員不能回避的話題。書中簡(jiǎn)單介紹了兩個(gè)基本方法,雖然詳細(xì)討論這兩個(gè)問題需要更大的篇幅,而且在實(shí)際工作中可能會(huì)采用BoundsChecker或Purify這樣的工具,但這種簡(jiǎn)單的方法比較有利于初次接觸這個(gè)領(lǐng)域的人理解概念,也容易進(jìn)行嘗試。
第二部分的主題是標(biāo)準(zhǔn)C++庫(kù)。面向?qū)ο笙到y(tǒng)通過復(fù)用來(lái)提高開發(fā)效率,標(biāo)準(zhǔn)庫(kù)則是實(shí)現(xiàn)復(fù)用的一個(gè)重要方面。作為一個(gè)Java程序員,筆者對(duì)這一點(diǎn)深有體會(huì)。
以前用Borland C++時(shí),筆者也用過String類,但那不屬于C++標(biāo)準(zhǔn),是廠商擴(kuò)展。學(xué)過C的人都知道,使用C的char*或char[]需要專門的練習(xí)。C++則利用對(duì)象封裝的力量,減輕了程序員的負(fù)擔(dān)。Java有String和StringBuffer兩個(gè)類,它們?cè)趯?shí)現(xiàn)原理上不同,適用的場(chǎng)合也不同,《Effective Java》一書中有詳細(xì)討論。C++只有string類,關(guān)于它的對(duì)象創(chuàng)建和內(nèi)存管理可以仔細(xì)研究一下,這涉及效率。首先考慮效率問題,是C/C++文化的“商標(biāo)”。要考慮效率,就需要對(duì)計(jì)算機(jī)的結(jié)構(gòu)和工作原理有充分的了解。一個(gè)深受C/C++文化影響的人,會(huì)不由自主地考慮每條語(yǔ)句后面計(jì)算機(jī)所做的事,并考慮這樣做是否有效率,而許多只學(xué)過Java的程序員則很難表現(xiàn)出這一特點(diǎn)。不過遺憾的是書中沒有附帶介紹正則表達(dá)式的使用,以筆者的經(jīng)驗(yàn),正則表達(dá)式是字符串操作的有力工具。
抽象出流的概念是面向?qū)ο笤O(shè)計(jì)的又一經(jīng)典范例,向我們展現(xiàn)了一個(gè)好的對(duì)象系統(tǒng)設(shè)計(jì)是多么易于理解。Java也繼承了這一概念,雖然實(shí)現(xiàn)上有差異。通過從istream和ostream多繼承得到iostream,這和十多年前筆者學(xué)到的一樣,只是現(xiàn)在這些類都已模板化,并成為了C++標(biāo)準(zhǔn)的一部分。對(duì)國(guó)際化和本地化的支持也是現(xiàn)代編程語(yǔ)言不可缺少的特征,今天的C++對(duì)寬字符也有了良好支持,處理漢字時(shí)不再像當(dāng)年那么麻煩了。
模板的威力令人印象深刻,C++標(biāo)準(zhǔn)中的許多東西都已構(gòu)架在模板的基礎(chǔ)上了,如string、auto_ptr、IO流和bitset等。C++社區(qū)對(duì)模板有著特殊濃厚的興趣。從C++開始引入模板至今,大家想出了各式各樣的精巧用法,甚至有“模板元編程”這樣的奇特用法。Java也在1.5版本中引入了模板泛型編程。
模板的使用也產(chǎn)生了些許問題。模板對(duì)編譯器帶來(lái)了相當(dāng)?shù)呢?fù)擔(dān),尤其惱人的是在編譯時(shí)難以提供準(zhǔn)確的出錯(cuò)信息。另外,大量使用模板的程序也向閱讀者提出了更高的要求(請(qǐng)嘗試讀一下STL的源代碼)。一些專家建議慎用模板,例如在《UML參考手冊(cè)》(第2版)的Template詞條中指出:“模板應(yīng)該慎用。在許多時(shí)候(如在C++中),它們完成的功能可以通過多態(tài)和泛化更好地實(shí)現(xiàn),使用模板只是基于一種追求不必要的效率的錯(cuò)誤的熱情。因?yàn)樗鼈兪巧善?,它們的結(jié)果并不總是顯而易見的。”
模板為開發(fā)者提供了這樣一種場(chǎng)景,即我定義了一個(gè)通用算法,歡迎您來(lái)使用它;我定義了一個(gè)參數(shù)化類,歡迎您來(lái)特化它。往深了說(shuō),這涉及面向?qū)ο笳軐W(xué):有沒有純粹的算法?有沒有純粹的數(shù)據(jù)?世界僅僅是由對(duì)象和行為組成,還是存在超越對(duì)象而獨(dú)立存在的法則,這些法則對(duì)許多類對(duì)象都有效?C++標(biāo)準(zhǔn)的設(shè)計(jì)者認(rèn)為這種法則是存在的,所以在設(shè)計(jì)中實(shí)現(xiàn)了通用算法。而在Java中,即使存在這種法則,也要放在某個(gè)Util類或靜態(tài)方法里去,寫成God.newtonRuleOne()這種樣子。這種做法讓不少Java程序員在理解Singleton或service oriented programming時(shí)存在困難,在使用Spring這樣的框架時(shí)也感到別扭。理解C++標(biāo)準(zhǔn)庫(kù)時(shí),也需要了解一點(diǎn)“C++哲學(xué)”。
容器類非常重要,所以人們一遍又一遍地實(shí)現(xiàn)它。《Borland C++ 3.0程序員指南》中就提到它提供了兩個(gè)容器類,其中一個(gè)是基于模板技術(shù)實(shí)現(xiàn)的。Java在提供泛型支持后,又重寫了容器類的代碼,而在此前,容器類已經(jīng)由著名的Joshua Bloch重寫了一遍。C++的容器類實(shí)現(xiàn)完整,有dequeue、stack、bitset這樣的類。流迭代器也是Java中沒有的概念。筆者感覺,C++中的迭代器概念更像“封裝過的指針”。
與Java SDK中提供的類和方法相比,C++的標(biāo)準(zhǔn)庫(kù)還是比較小的。但網(wǎng)上有不少C++的庫(kù)可用,只是沒有像Java那樣形成標(biāo)準(zhǔn)。BOOST是一個(gè)“半官方”性質(zhì)的C++類庫(kù),試想C++標(biāo)準(zhǔn)的制定者們一定也在考慮,有哪些東西值得放進(jìn)C++標(biāo)準(zhǔn)庫(kù)中。
書中第三部分討論了一些擴(kuò)展主題,在實(shí)際工作中碰到的機(jī)率也比較大。
RTTI是“反OO”的。在JUnit框架中,通過使用反射機(jī)制,提供了一種“非OO”的擴(kuò)展方式,即TestCase子類中所有以“test”開頭的方法都會(huì)被框架當(dāng)作測(cè)試執(zhí)行。Xstream通過反射,可以訪問傳入?yún)?shù)中的私有屬性。在一個(gè)方法中,可以利用RTTI對(duì)傳入?yún)?shù)進(jìn)行類型檢查,然后用switch語(yǔ)句對(duì)具體類型進(jìn)行分別處理,從而導(dǎo)致沒有擴(kuò)展性的設(shè)計(jì),即犧牲了OO的多態(tài)性。然而,為了達(dá)到特殊的目的,您可以考慮這種犧牲。另外,使用RTTI也可能影響性能。設(shè)計(jì)即折衷。
多重繼承屬于那種聽起來(lái)很美的概念,要在自已的設(shè)計(jì)中使用多繼承,一不小心就會(huì)帶來(lái)許多麻煩。所以Java中沒有多繼承。在C++中,筆者也會(huì)考慮使用純虛類來(lái)實(shí)現(xiàn)類似Java接口的概念,這樣能避免多繼承的諸多問題。
設(shè)計(jì)模式已經(jīng)成為一個(gè)職業(yè)程序員必須掌握的知識(shí)。書中提供了關(guān)于設(shè)計(jì)模式的一些擴(kuò)展討論。讀這部分內(nèi)容之前,最好已經(jīng)研讀過那本經(jīng)典的《設(shè)計(jì)模式》。
有人斷言,并發(fā)是自O(shè)O以來(lái)程序員應(yīng)該掌握的重要概念。因?yàn)镃PU主頻的增長(zhǎng)已經(jīng)遇到了“天花板”,CPU廠商紛紛推出多處理器系統(tǒng)或多內(nèi)核系統(tǒng)來(lái)提高CPU的能力。為什么要在程序中使用并發(fā)?因?yàn)椋海?)CPU要等待IO,特別是網(wǎng)絡(luò)IO;(2)我們要充分利用SMP、Dual Core和Quad的能力。第一點(diǎn)還能通過異步IO來(lái)解決,第二點(diǎn)就能只能靠并發(fā)了。在Java中,一開始就提供了多線程支持,在1.5版本中更是增加了最初由Doug Lea設(shè)計(jì)的并發(fā)包。在頭腦中建立起并發(fā)程序設(shè)計(jì)的概念是不容易的,所以線程安全和synchronized關(guān)鍵字一直是檢驗(yàn)Java程序員水平的試金石。Bruce Eckel他們也認(rèn)識(shí)到了并發(fā)程序設(shè)計(jì)的重要性,只是在C++標(biāo)準(zhǔn)中還沒有對(duì)并發(fā)的支持。書中通過一個(gè)開源的Zthread項(xiàng)目,介紹了并發(fā)編程的一些內(nèi)容。
學(xué)習(xí)一門語(yǔ)言時(shí),掌握其語(yǔ)法只是一部分工作,更值得關(guān)注的是熟練使用這種語(yǔ)言的人們?nèi)绾芜\(yùn)用語(yǔ)言的元素來(lái)表達(dá)他們的思想。與語(yǔ)法相比,一些習(xí)語(yǔ)和典故才是語(yǔ)言中更具魅力的部分。這本書在有限的篇幅內(nèi),向我們展示了C++社區(qū)的人們是如何使用C++的。
C++可能是最難學(xué)的編程語(yǔ)言之一,要學(xué)好它,需要講究方法。筆者曾經(jīng)認(rèn)真思考過語(yǔ)言學(xué)習(xí)的奧秘,最后得到的秘訣是八個(gè)字:聽說(shuō)領(lǐng)先,讀寫跟上。對(duì)于自然語(yǔ)言的學(xué)習(xí)和編程語(yǔ)言的學(xué)習(xí)都是如此。聽、讀就是學(xué)習(xí)已經(jīng)掌握這門語(yǔ)言的人如何使用它,說(shuō)、寫就是自己實(shí)踐?!拔釃L終日而思矣,不如須臾之所學(xué)也?!盨cott Ambler曾在他的著作《過程模式》中介紹過學(xué)習(xí)一門語(yǔ)言的方法:效率最高的方法是找到一個(gè)有資質(zhì)的老師,參加他的課程;其次是找到一本好的教材,自己系統(tǒng)地學(xué)習(xí);效率最低的方法是不看書,自己拿一個(gè)試驗(yàn)項(xiàng)目開始折騰。
這本書的中文版翻譯質(zhì)量比較好,但也存在一些瑕疵,如在講解派生類的契約時(shí)將“Require no more;promise no less”譯成了“不要只索取不付出”,筆者認(rèn)為譯成“要求不能多,承諾不能少”更合適。又如第10章中“四人幫”的譯法,值得商榷。譯文總體上意思準(zhǔn)確,語(yǔ)句通順,不會(huì)影響閱讀。
總之,這本書既適合作為初學(xué)者學(xué)習(xí)C++的教材,也適合像筆者這樣想了解C++新規(guī)范的人。在內(nèi)容處理上符合本書的定位:一本C++培訓(xùn)教材。如果說(shuō)“在任何領(lǐng)域,讀5本書入門,讀50本書成為專家”,那么這本書可以列入5本書的范圍之內(nèi),當(dāng)然也能成為50本書之一。
C++兼容并蓄,C++力求簡(jiǎn)約,C++博大精深,C++有特殊的美,所以說(shuō):閎約深美C++。筆者看到C++從Java的發(fā)展中吸收了經(jīng)驗(yàn),在下一個(gè)標(biāo)準(zhǔn)版本中,肯定會(huì)帶來(lái)更多的東西。筆者也希望有一天能有機(jī)會(huì)再用C++寫東西。