C++爲什麼成功?

第一部分

不客氣地說,CSDN論壇有一股不太好的風氣,那就是喜歡空對空。常常就常識性的問題爭個不可開交,而真正有價值的帖子卻鮮有人問津。這樣就很難留住真正有技術積累的且樂於貢獻於社區的工程師。而且,由於基本的問題被一遍又一遍地問,總給人一種在低水平徘徊的感覺。道理上講,還有一種可能是有太多的新人不斷加入到C++之中,從而是C++的平均水平被稀釋。我情願是這樣。同時,我真心希望,這一代C++工程師不是通過CSDN的C++版來學習C++的,至少不僅僅是。

書歸正傳。今天,偶然看到了了這個帖子《從號稱自己C++好的人說自己C語言很差說開去》[url=http://topic.csdn.net/u/20091011/05/492fe17d-63b8-4050-a677-3b536908d8a7.html][/url],不出所料,其熱鬧程度非同一般。本來類似的問題也容易引起大家的共鳴和爭論,我們可以從中抽提重一點有價值的東西。但是通篇看下來,有價值的評論沒有反響,明顯錯誤的卻引來熱捧。今天,索性花點時間,不揣冒昧,就其中謬種流傳給出一個剖析。這種工作其實是喫力不討好的,在專業工程師眼中是等而下之甚至不入流。那我爲什麼做呢?
1. 我對自己有一個承諾,有貢獻於社區,因爲我從社區獲取良多。所以不敢偷懶
2. 專家一般不會在這種問題上浪費時間。我不是專家,儘管時間還是寶貴,但是如果它能有用,我就覺得值得
3. 期望能引起思考,進而早日上層次(沒有寫錯!)

1. C++是用來解決什麼問題的?
支持或者維護過稍微大一點的項目就會明白,理解現有的代碼是最困難的事情。這個困難不是在於你熟悉不熟悉某個庫(專用庫往往是最容易理解的),而是理解其業務邏輯。說白了,就是類似反彙編當初編碼人員的想法。這裏,程序或者代碼本身的組織帶來的複雜性可能已經超過了業務邏輯的複雜性。而且,這種複雜性不是線性增長的!複雜性的來源是一個有趣也是開放的話題,這裏僅僅列出一部分,並且看看C++是怎麼進步來控制這些複雜性的:
a. 單一作用域內過多可見的名字。過多的名字不僅僅導致名字衝突的機率急劇增加,而且使得該作用域內任何的操作可能波及到範圍指數變大。這是我們一直強調儘量不使用全局名字的原因。C提供了結構這樣的操作,把多個關聯的名字綁定到一個名字;C++除了通過使用訪問控制來強化名字使用之外,還提供了名字空間的構造,可以把不是直接關聯,但是邏輯相關的名字隱藏起來。這也是爲什麼C++允許就近聲明。

b. 細節完全可見。C++使用訪問控制還有一個好處就是隱藏細節。人們往往難以經受走捷徑的誘惑而太多暴露是細節很容易讓一個程序員在不完全理解的情況下使用了他不應該使用的東西。不要指望通過完善文檔來避免這樣的事情,因爲一來程序員寫的文檔從來不夠更新,二來程序員更相信自己的直覺。這就是我們強調“自文檔”的含義,儘量使你的代碼通俗到不用寫註釋也可以完整表達你的含義。這就要求我們使用合適的名字,這也是爲什麼熟悉const關鍵字成爲C++入門的必須課,也可以解釋爲什麼還要引入專門的mutable關鍵字。一個常識是代碼是寫給程序員的,而不是編譯器;

c. 相同功能的不同非標準的實現。要想理解非標準的實現,就要深入到其中細節。這對於理解代碼簡直就是夢魘。更糟糕的是,許多以自己實現字符串類,容器類爲驕傲的程序員的實現根本達不到標準的高度,更不要說超出。這裏的常識是(如果你不是輪胎廠商就)不要重新發明輪子。這裏可以解釋爲什麼C++在語言核心的改進非常謹慎,而在庫的支持上不斷拓展而精益求精。其他現代語言以豐富的庫支持而自豪,自稱爲C++程序員的人卻抱殘守缺,以不用任何標準庫爲榮,真是怪事哪都有,這裏特別多。較新C++教學資料,如C++ Primer, Accelerate C++以及Programming Principles  and Practice Using C++都完整的OO以及C++標準庫爲基礎。忘記譚浩強先生的C++入門書吧。

d. 資源管理。資源是使用之前需要申請,使用後必須釋放的東西。資源由於其稀缺,所以就顯得非常重要。最簡單的資源就是內存。C程序以及大部分不好的C++程序中,內存問題一直都是佔據主要。C++提供了自動調用構造函數,析構函數以及基於值語義的可重載的對象複製機制,進而使得資源管理問題前所未有的簡單。這個技術簡稱RAII,是學習C++登堂入室的關鍵。熟練使用這個看似簡單的技術,就再也不會遭遇資源泄漏,資源重複釋放,資源釋放後使用等等一系列問題。

e. 錯誤處理。如果這個問題沒有引起你的注意,那麼你就沒有真正寫過工業強度的代碼,但就是要求可以7*24運行,可以用於諸如生命保障系統控制的代碼,儘管巨大部分的代碼都不需要這樣的強度。錯誤處理之所以複雜在於我們在出錯的時候要儘可能回覆到先前的狀態,比如一個多線程服務程序,當一個新的請求因爲資源有限而不能啓動線程提供服務時,其他的正在運行的服務線程不應該收到任何的影響。C用來克服這些問題的手段有goto這樣的局部跳轉以及long jump這樣的全局跳轉。經驗證明這些構造都不好使用(副作用大,難以組合等)。大家都知道,C++提供了異常。配合上述的資源管理,二者真實雙劍合壁,誰與爭鋒。那些提起異常就考慮性能的可能一直都不明白,異常構造究竟是用來幹什麼的!

f. 風格和組織。C和C++都是自由風格代碼,所有差別不大。但是我們有合適的編碼標準。使用一致的編碼標準,可以強化代碼閱讀人員的閱讀習慣,減少歧義,提高精神歡愉程度而提升工作效率。這個C和C++打個平手。

g. 問題分解。複雜的問題必須首先分解之,然後各個擊破。這也是團隊合作的基礎和前提。做這個目前所知的最好的工具就是OO。C++提供了對OO的完整支持,這就使得C++這個領域得心應手,左右逢源。GoF在Design Pattern中說,選擇支持面向對象的語言的原因之一就是不想論述諸如繼承,運行時多態這樣的模式,儘管這樣的模式在C的實現中非常普遍。C++把C中太常用的模式標準化了。

h. 靜態類型檢查。強類型之所以有用,是因爲我們需要編譯器幫助我們檢查可能在實現引入的bug。C也是強類型的,但是支持的不夠(想象全能的void*)。C++通過引入類型安全的庫支持以及細化了類型查遍而強化了強類型檢查,同時,是用有效的泛型手段,在加強了靜態類型檢查的同時還不需要額外的代碼。

i. 有效的封裝可以大大降低問題的複雜性。人人都知道,Windows平臺上CreateFile是一個重要的函數,但是誰能在不查看文檔的情緒下寫好這個API的調用?那麼istream呢?這個例子形象地說明了封裝的極其重要而且C++善於與此。從系統API和標準庫流差距十萬八千里,其中你可以發揮餘地的地方很多,比如創建NT sparse file就必須使用系統API,但是如果你每次都需要查文檔,那麼那還不是合格的程序員,更不要說是合格的C++程序員了。所以這裏就顯示出了封裝的重要性。我評價一個C++程序是不是成熟的標準之一就是有沒有自己的C++封裝庫。封裝不僅僅積累類的知識,還有效減少了名字的數量。一個系統API往往需要多個參數,使用C++的缺省參數機制以及設計專用類,可以有效降低程序員的記憶負擔。而且,除非重構了代碼,你都不需要做重複的測試!

這樣的條目還可以列出一些。基本上,任何一個C++相對C的新特性,都可以找到對應的工業最佳實踐。

2。C++和性能問題。
C++程序員在更新的系統語言之前有時候不夠自信,原因是C++太複雜。但是他們還有一根救命稻草,那就是性能。可惜的是,C++從來不是靠着宣稱自己性能如何而獲得目前的地位的。下面的敘述屬於常識。對性能問題有一般理解的都可以直接跳過不讀。這是爲C++新手準備的,只要是爲了打破性能迷思,重歸學習C++的康莊大道。

a. 常識之一:性能是設計出來的,不是實現出來的。這句話的意思不是說糟糕的實現也能實現優良的性能,而是說,再好的實現,包括語言的選擇,導致的性能改善都不如設計時對性能的考慮。現實的情況是,真正性能攸關的部分,可能早就在設計之初做過評估並且有了原型實現。實現時選擇語言的機會非常有限。所以最後的結論就是C++在項目中被選中的絕大部分原因並不是C++的性能有多好。

b. 常識之二:未成熟的優化是罪惡之源。這句話的意思表述的和上一個有點相似,但是是從另一個側面。那就是,除非做過性能瓶頸的測試(profiling),永遠不要直觀猜測性能的瓶頸,否則你可能花大量時間優化了不需要優化的地方。真正的性能瓶頸一般只存在與非常有限的幾個分散地方,程序員對此估計的不準確性是人所共知的。

c. 常識之三:對於真正的性能問題,數據結構和算法的選擇是關鍵。這需要多性能場景做數據流分析才能確定使用什麼算法或者動態調整算法。C++非常關注性能問題,所以有基於樹的map,也有機遇散列的map;有支持前端後端快速插入的容器,也有可以再中間快速插入的容器,還有可以根據迭代器類型自動選擇二分查找還是順序查找的自適應的標準庫。這些,保證了一般C++代碼的性能不會低於同等質量的任何其他代碼。

d. 常識之四:在性能成爲爲題之前,它不是問題。這個有點繞。其基本含義是,對於絕大部分的C++實現者,也就是我們一般所說的C++程序員或者工程師,性能可以是最後一個考慮的因子。在此之前,很多更重要的事情需要關注,如編碼風格,函數封裝,基本類的設計以及C++基本構造的有效和合理使用。

e. 常識之五:C++不靠性能取勝,學習使用C++也並不複雜!這可以讓C++工程師在C#和java程序員前挺直胸膛。C++解決的問題是偏向於底層的,複雜的系統及的問題,它的成功是靠着有效的複雜性控制機制以及與系統平臺的天然兼容。而且,它正變得越來越易用!

3. C++和C
C++和C血脈相連,不可分割。二者不是競爭關係,而是相輔相成的,這種C和C++標準的相互借鑑就可以看出來。C++不是唯一的從C派生出來的現代多模式語言,但是C++是唯一成功的。下面就所謂的C和C++的複雜性做一簡單討論。希望可以給那些正在學些這些的朋友們一點幫助。

從C++的角度而言,C沒有什麼複雜性,一切都簡單透明。C的複雜性來源於其基本特性的組合,比如數組,指針,函數指針的組合。在稍微複雜一點的應用中,它們的組合不可避免。另一方面,由於C處理的問題大部分在系統編程領域,系統編程有好多特性的約定,也導致C奇怪的使用。這應該屬於域知識,並不是C的一部分。好多自動化問題的解決方案都是以C作爲中間代碼,依筆者有限的經驗,這個可以說是相當的容易。

C中最複雜的毫無疑問是指針。但是複雜的並不是指針本身,而是指針可以做的工作太多太多,以及C語言中對指針操作的任意支持。一般來說,《C專家編程》是任何C背景程序員的必讀數目。另一方面,閱讀以下C規範,理解什麼是標準明確定義的行爲,這樣就不至於侷限於編譯器的實現。當然使用多編譯器是最好的,可以理解C的實現之差別。關注C語言構造背後的東西(rationale)而不是語言細節,並且熟悉C標準庫。

C++,給人的第一影響就是複雜,複雜,複雜。但是,對於滿漢全席,您會嫌盤子多嗎?C++之所以又複雜的語言構造部分原因是它要解決的問題域極其寬廣,部分原因是歷史造成了。好消息是C++正在變的易於使用而且C++入門書籍也愈來愈好;不幸的是C++還沒有完全達到人因工程要求的好的程度(易於寫正確的代碼,而不易於犯錯)。我們學習C++的目的是要使用它,就像買個電視是爲了看CCTV,而不是研究電視的工作原理一樣。


4. 總結。
夠長的了。上文中提到並且批評了一些錯誤的理解和思想,希望那個不至於誤會爲針對個人。作爲一個使用C++的程序員,我的大部分日常代碼都是使用perl寫的,包括生成C++代碼的時候。當有人責難爲什麼C++0x要引入regex的時候,我爲此歡呼,因爲它確實有用而,實現良好,而且與perl全兼容:)

本文的主要目的不是拋磚引玉,而是作爲期望作爲一個可以代言那部分沒有發言的C++程序員,提供可以讓C++新生力量少走彎路的一點指南。其中除了錯誤,其他所有都來於這個虛擬的社區。
————————————————
版權聲明:本文爲CSDN博主「bfzhao」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/bfzhao/article/details/4673087

 

第二部分

在上一部分中,我花了大量的精力強烈暗示使得C++如此成功的並不是由於所謂性能上的優勢,而是其規範並昇華了C的最佳實踐。現在我要用同樣的精力來證明C++在性能上的專門考慮和設計以及實踐中的優越表現。C++根植與C的肥沃土壤,天然具有C所擁有的直接與物理硬件交互的優勢。更要提到的是,每一個C++新特性的引入,無不是經過了詳細的性能權衡。毫不奇怪,C++在絕大多數的情況下可以取得與C比肩的性能,並且有所超過。

 

性能的最終瓶頸必然取決於硬件能力。這樣,優化性能的途徑不外乎兩點:

1. 充分發揮硬件的優勢。

2. 不做無用功。

 

在第一點上,C和C++不分伯仲。原因是,編譯器後端使用的往往一個完整支持的CPU集合的大部分公共指令,這就意味着某種可以用於特定CPU的特別優化指令根本就不可能出現在編譯後的二進制代碼中。這同時也就說明了爲什麼需要內聯的彙編,而C和C++都完美支持這個特性。另一方面,C和C++編譯器一般使用同樣的後端,這樣更模糊了C和C++的界限。同樣有效的C代碼,使用C++編譯器和C編譯器產生的最終結果基本完全一致,甚至更好,後面有詳述。

 

第二點看起來非常簡單,但是由於做無用功的可能性成千上萬,想做到不做無用功也就比初看起來的要困難的多。我們先簡單分析一下這是爲什麼。第一,現代基於模塊的編程實踐非常強調封裝,這基本上都是以一定的性能損失爲代價的。對於編譯器來說,那就是,你的代碼不可能超出編譯器和其基本庫的實現限制;第二,選擇合適的算法有時候很難,這決定了實現的性能表現。本質上,就是使用更好的避免的做無用功的方法。使用簡單的排序算法說明。冒泡算法之所有低效,是因爲儘管我們通過比較知道a>b,b>c,但是我們還需要比較a和c的關係,其實這完全是浪費。快速排序則不同,它使用一個值來吧整個排序集合分爲兩部分,大於它的和小與它的。這樣,這兩個集合中的任何元素都不必在進行比較,這就是不做無用功。仔細思考一下其他的常用算法,你就可以很清晰地認識到都是這樣。這就是我們一直強調算法是性能的關鍵的原因,因爲標準算法都是經過詳細設計的,實踐證明正確的,做無用功最少的代碼抽象。第三,無論從何種尺度,避免重複都要比預期的要困難。重複可能表現爲:數據重複(同樣程序狀態被保存在多個位置,它們之間需要更新和同步),執行重複(由於諸如緩存頻繁失敗而導致的低性能代碼被重複調用),結構重複(同樣的代碼被重複執行而具有相同的副作用)等,這僅僅是運行態的一般情況。實現的重複則會導致的執行體體積龐大,引用冗餘,在導致性能問題的同時引出維護的災難。

 

其實這些都說明,在相同的層面上,性能其實和你選擇工具語言沒有什麼直接的關係。C和C++無疑是在相同的層面上的。我們已經知道,C++提供了現代的構造,滿足超大規模的設計和實現的基本要求。但是,C++是怎麼在提升代碼可管理性的基礎上,可以保證與C基本一致(差別小於5%)的性能的呢?

 

1. 使用(更)強的類型。C++和C都是強類型的語言,但是C++做的更好。強類型帶來的好處是,類型判斷和檢查可以在編譯期執行,而不是運行時。這就意味着目標代碼的體積更小,邏輯更簡單。

 

2. 需要時再計算(延遲求值)。這是最常用的常規性能優化方法。程序的一致執行會話中,可能某些代碼段並不會被執行到,那麼這些代碼所需要的數據就沒有必要產生或者計算。mutable和const關鍵字提供了基本的支持。

 

3. 需要時才聲明對象。這樣可以避免當條件不滿足時構造和銷燬對象的開銷,同時保證的最小的對象作用域。

 

4. 避免沒必要的對象複製。返回對象的函數調用一般都會產生一個臨時對象,當函數調用完畢之後,該臨時對象就會被銷燬。這雖然完全符合C++ 的對象語義,但是是低效的。一般的C++編譯器都會執行返回值優化來消除這個額外的對象構造。從C++語言來說,這樣的臨時變量其實可以綁定到一個常量引用,在該常量引用失效之前,這個零時對象一直存在。然而,仍然存在着一些場景,會不可避免地產生中間對象的而導致的低效,C++0x通過引入右值引用來試圖消除這個最後的可能性。

 

5. 基於模板的標準程序庫和算法。這是C++中最令人叫絕的地方。它是執行效率和易用性的完美折中。和C基於值語義類似,所用的標準容器都保存一份數據或者數據指針的拷貝。模板在避免了大量的代碼重複的同時,通過完全媲美手寫算法的性能,這就是C++語言。

 

C++在性能問題上被人詬病,除了明顯的無知,還有一些深層次的原因。那就是對C++特性的濫用。這裏討論幾個最常聽到的arguments:

a. C++的虛函數機制。可能你常常看到有人煞有介事地評論調用一個虛函數會導致一直額外的查表操作,這個是如何導致了他的代碼的性能低下。事實是:除非他的代碼根本不需要運行時多態而濫用了虛擬機制,否則C++編譯器的實現基本上可以說是最直接且高效的,比如使用散列來索引函數的地址。恰當的比喻是,你要切牛肉,非要使用關老爺的青龍偃月,還要抱怨刀鋒劃了案板。

 

b. 異常機制。異常恐怕是C++裏最沒有被廣泛使用的最好的特性了。由於異常要求非常不同的程序觀念,有一些公司甚至如洪水猛獸般避之不及。不幸的是,C++標準庫也使用了異常,那就意味着C++標準庫最好也就不要碰了。異常導致的問題主要是代碼執行管理和軟件架構中錯誤處理策略的更本性變革,但是從來不是因爲其性能問題。是的,異常的拋出和捕獲會導致低性能。但是真正需要異常發揮作用的地方可能只有程序執行邏輯的1%。形象的比喻是,如果過年喫一頓好的也被認爲是浪費,那就是不辨是非了。

 

c. 對象的自動構造,析構和複製。複製我們不說,因爲C也支持數據結構(struct)的直接賦值。對象的自動構造和析構之所以必要,因爲在某些情況下,我們需要。當我們不需要的時候,它們完全可以是沒有的。如果可以理解C中要求變量聲明後必須賦初值的最佳實踐,也就可以明白C++怎麼把這樣的最佳時間制度化了。

 

該是那就老話,性能是設計出來的,不是實現出來的。有關C++性能的討論可以休矣!
————————————————
版權聲明:本文爲CSDN博主「bfzhao」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/bfzhao/article/details/4700653

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