第14章
- 趙岩(哈工大)
後記
14.1 程式的效率
每個人都希望自己的程式有效率,這無可厚非。就像我們每個人都希望自己能夠漂亮一點兒一樣。如果你是一個演員,漂亮對你來說至關重要。不過如果你是一個在荒島上的程式員,那麼很明顯,能上網要比漂亮更有意義。希望程式有效率,這沒有錯,但是别忽視了應用環境。首先要問自己一個問題:“我的程式是否是時間敏感的?”
如果我用非常簡單明了、直接的程式可以解決相應的問題,是否就不滿足效率的要求了?千萬别忘了,計算機幹别的真不行,但是計算起來真的很快。
舉例來說,一個包含一萬個詞的詞典,滿足使用者基本的增加、删除和查找功能,你用最低效的數組和最高效的紅黑樹,對于最終的使用者來說,根本感覺不到差别。
唯一的差别就是,對程式員來說,用紅黑樹實作,代碼更多,開發周期更長,開發成本更高,代碼更易出錯。是以,在關注程式的效率問題之前,你首先要從面臨的問題出發,問一問自己,效率是不是這個程式的瓶頸所在。完全不考慮對應的應用,隻是在堆砌自己所學的知識,這個在《重構:改善既有代碼的設計》[13]一書中有一個非常專業的稱呼,叫“過設計”。
如果處理的問題數量級很大,或者是工業控制類時間敏感性程式,那麼就不能簡單地忽略效率的問題了。程式的效率歸根結底在算法上,一個計算複雜度是O(n2)的算法,無論多麼精巧地來實作,無論應用多少提高效率的小竅門,效率也不會高過一個複雜度為n的算法。這個問題出在根本上,就像你是老鼠王國裡的冠軍,也畢竟是個老鼠,也會被一隻哪怕最瘦弱的獅子殺死。是以如果我們探讨程式的效率,第一個問題就是要選擇合适的算法。
這又回到了算法的問題上了,有些同學一看算法就頭疼,這我能了解,我現在看算法也頭痛。但是沒辦法,任何學科、任何工作,抛去風光的外衣,都有一個堅硬的核,看你能不能咬穿它。算法就是計算機科學中最堅硬的殼,你必須咬穿它才能吃到裡面的果實,最好像我一樣,每天都去咬一咬。
好了,假設你已經咬穿它了,針對任何問題,都已經能找到非常合适的算法,下面就是在編碼層次上進行效率的提升,這個時候,需要注意一點。哲學上有一個著名的原則,叫做8-2原則,也叫冰山原則。冰山原則的名字來源于這樣一個事實,一座浮在水面的冰山,水面上通常隻露出20%,水面下還隐藏着80%。由此推廣開來,一個團隊中20%的人會完成整個團隊的80%的工作,就像西遊團隊的孫悟空,80%的妖精都是他一個人收拾的。我們再進一步推廣到程式,一個程式的80%的時間都是在運作20%的代碼。試圖優化一個程式的時候,我們應該首先優化那20%的代碼,這樣才是最有效率的。這20%的代碼段被稱為“Hot Spots”。目前有很多檢測工具可以幫助定位那20%的代碼,例如VS2005以後自帶的性能分析工具Performance Profiler以及第三方的工具dotTrace等。
假設你已經通過工具發現了“Hot Spots”,終于可以利用我們以前學過的源碼級的優化技巧來對源代碼進行優化了。且慢,我們大家都知道i++; 的效率要比i=i+1; 高,i>>1; 效率要比i/2; 高等。不過坦白地說,目前主流的編譯器都會自動地把i/2; 轉換成i>>1; 的,是以如果檢視它們生成的彙編語言代碼,你會發現這兩段代碼生成的彙編語言代碼都是一樣的,但是對程式員來說,i/2; 能更好地表達出開發者的本意,而i>>1; 這一段代碼卻會讓一個新程式員一頭霧水。這麼來說,這種優化就沒什麼搞頭了。不僅如此,一個好的編譯器會做很多方面的優化,涵蓋了很多内容,是以選擇一個好的編譯器并打開對應的優化開關進行編譯,比自己手動優化更有效率。
就算是編譯器沒有做優化,比如把一個函數調用變成直接的一段調用,雖然避免了函數調用的棧操作代價,但也明顯違反了子產品化的原則。另外有些優化手段也是不可移植的,是以說優化問題是一個雙刃劍,有利就有弊。
說到這裡,如果你依然對優化和效率非常執着,那我不得不佩服你了。反正我一般程式設計式的時候主要就是考慮思路和正确性,很少考慮優化的問題。
14.2 C語言的使用原則
對于剛剛接觸程式設計的學生,有的老師建議不應該首先教授C語言,而是應該教授Python等腳本語言,如果就是為了教會同學們編寫程式,C語言确實有點難了,簡單明了的腳本語言可能更适合。我不反對這種觀點,但是我有點兒不同的意見。C語言之是以難,是因為它要解決更難的問題,是以必須更底層、更靈活,也是以它必須引入指針、位運算等。如果問題本身很簡單,那麼使用C語言來完成也可以像使用Python語言來完成一樣簡單,這取決于使用方法和使用政策。
是以,使用C語言的一個最基本的原則就是盡量使用“簡單的C語言”,不要過分炫耀和使用一些不常用的特性和技巧,這些特性和技巧也許根本不适合解決你的問題,而且還可能引入潛在的、難以發現的bug,造成“C語言很難”這種錯誤認識。
例如,如果想通過定義一個宏來完成交換兩個變量的功能,貌似簡單,但這絕對不是一個簡單任務。你也可以用C語言支援的位操作中的異或操作交換兩個變量,這種晦澀的寫法确實可以讓人印象深刻,但是印象深刻并不總是褒義詞。用一個臨時變量通過三次指派來完成交換,這種簡單的方法雖然不酷,但是一般不會出錯。
是以,我建議你把精力放到那些需要C語言解決的問題上,而不是放在語言本身。“簡單的C語言”隻是一個原則,同樣的概念是“Keep simple,keep stupid”。這個原則說白了就是用最簡單的方法達到自己的目的。C語言中,實施這一原則有很多具體的建議,例如不寫長表達式,如果表達式很長就用臨時變量;多用括号确定運算優先級;一個函數隻做一件事;定義指針變量時就初始化。最關鍵的是,要養成用簡單辦法解決問題的習慣,這樣可以規避很多C語言程式的錯誤。
同時,你最好能保留一些經典的代碼片段(snippet)。在本書中,4.7節中的位操作宏,以及8.5節中的模拟撲克的洗牌程式,都是比較有價值的snippet。學習這種snippet可以提高水準,開闊思路;保留這種經典的snippet可以友善今後進行複用。
14.3 加深對C語言的了解
如果你對本書的所有知識都已經掌握,而且書中的所有源代碼你都親自編寫并實驗了一遍,那麼我相信,你可以比較流暢地使用C語言了。
但是别高興得太早。當我完成這本書的時候,我并沒有“蕩胸生層雲”的成就感,而是有點小恐懼。這不是故作姿态的謙虛,而是真實的感受。伴随着不斷地查資料,不斷地總結,不斷地看到各種奇思妙想,我越來越感到C語言的博大精深,越來越感到自己的無知。學習就像一個不斷擴大的圈,随着圈子的不斷擴大,才會知道圈外的未知世界原來這麼大!
一直到目前為止,我們都是在使用C語言,但是并不了解C語言。我告訴大家要這樣用,不要那樣用,但是很少解釋為什麼要這樣用或者為什麼不那樣用。林語堂曾經說過,“隻用一樣東西,不明白他的道理,實在不高明”,說的就是我們這種狀态了。
那如何才能變得“高明”呢?别忘了,C語言要經過編譯器的編譯,才會變成可執行程式。是以有的時候,你是如何了解C語言的并不重要,編譯器是如何了解C語言的才是終極的答案。
例如,程式14-1中有兩個函數f1和f2。f1函數中,printf并沒有列印出數組a的長度,而是隻列印出一個4。這是因為編譯器會把傳入這個函數的a數組變量轉變為一個指針變量。是以sizeof(a)隻會列印出一個指針變量的尺寸,那就是4。了解了這一點,就可以了解第二個函數f2中的代碼。如果不是定義數組時的初始化,C語言不允許對一個數組變量直接指派,但是程式14-1中第7行卻可以,這也是因為這個時候str已經是一個指針變量了。
程式14-1 編譯器對程式的了解
對編譯原理的學習和了解,可以幫助我們真正地從核心了解C語言,而不再被語言本身的外衣所蒙蔽。
本書在第10章濃墨重彩地介紹了指針,貌似寫的天花亂墜,其實我内心真的很發虛。因為我根本不知道編譯器看到int *p這個語句後,是如何了解它并轉換成對應的彙編語言的。這部分知識就像是一個被紅蓋頭蓋住了的新娘,讓我充滿了好奇和沖動。本來想從編譯器的角度來介紹一下指針,但是發現我的這一部分知識不足以把這個事情闡述清楚,可以這麼說,這是本書的一個遺憾。如果本書有機會出第2版,我一定要把這一部分的知識加上去。
14.4 C、C++以及C#(java)
江山代有才人出 各領風騷數百年,在計算機程式設計語言方面,短短的不到50年的時間裡,主流的開發語言已經進化了三代,它們分别是C語言為代表的面向過程的語言,以及C++為代表的面向對象的語言,以及以Java和C#為代表的基于虛拟機的語言。
每一次的進化,都是對上一代語言的優點的繼承,同時對其缺點又加以了改造和改進。C++語言繼承了幾乎所有C語言的文法和庫函數,同時為了提高語言的封裝,繼承和多态,C++引入了類的概念。同時,也引進了第13章中我們介紹過的異常處理。在C#和java中,完全繼承了C++的面向對象的設計思想,同時放棄了C++和C中備受指責的指針,而采用虛拟機的方式來自動地管理和回收記憶體。在一些細節方面,C#語言更是針對C語言的缺點進行了革新,例如,C#語言會自動檢測數組越界,同時C#語言引入decimal用于需要更多有效位的浮點數應用,C#還取消頭檔案以避免C語言的重複包含的問題等等。
有趣的是,雖然有針對性地對上一代語言的缺點加以改造,但是目前這三代語言并沒有完全取代的意思,而是形成了一種相安無事,優勢互補的一種局面。我們在第2章也介紹過,這三種語言所占的份額也基本持平。這是因為所謂的缺點,在另外一種情景下,可能就是優點。就像指針,雖然會造成很多麻煩,但是處理一些資料結構的時候,還是最友善和最有效率的工具。虛拟機雖然幫助你自動管理記憶體,但是它使得程式的運作速度下降,雖然對虛拟機進行了各種優化和改進,但是C#語言至少要比C++慢一半,有的時候甚至更多。是以對這三代語言,正确的态度就是充分了解每種語言的特性,然後在正确的場景下,使用正确的語言。一般情況下,C#或java主要用于編寫直接面向使用者的各種(GUI)應用程式,C++多用于開發各種背景使用的算法和邏輯庫。而C語言則更底層,主要用于開發更核心的算法和靠近硬體的各種驅動程式和控制程式等。
這雖然是一本寫C語言的書,但我必須承認,C#和Java目前使用得比C和C++語言廣泛,而且比起C++也容易學習和使用。但是别忘了,好學就意味着C#和Java的程式員要比C++的程式員多。是以有一天你發現公司招聘C/C++程式員,你的第一個問題就是,他為什麼不去招一個人數更多的Java程式員?這個問題的答案有兩個,第一,他要更快;第二,他要更底層。這兩點C#和Java都不勝任。是以說,作為一個C/C++程式員,你完全有資格要一個高價錢。
作為一個合格的程式員,我認為應該很好的掌握這三代語言才行。大家也不用感到害怕和沮喪。如果讓你學三門外語,你可能覺得很困難,不同的外語之間差别太大了。但是這三代計算機語言,差别并不是你想的那麼大。尤其是一些有關語言的基本的概念,其實根本沒有變。一旦你學會了一門語言,那麼其他的另外兩門語言,很快也就融會貫通了。例如,在C語言中,我們介紹了函數指針的概念,如果你把這個概念了解透徹了,你會發現C#語言中的代理(delegate),其實就是一個函數指針。反之,如果你根本沒學過函數指針,C#語言中的代理(delegate)就會讓你感到非常的難以了解了。是以說如果你把一門語言學通,其他的語言一個禮拜搞定,也并不是什麼難事。從這個角度來說,C語言确實是一門很好的入門語言,雖然比較難,但是如果學會了,就等于一下子學了三門語言,而且還可以通過這門語言把很多基本的計算機概念搞明白,實在是成本效益超高啊!
當然,你也别高興的太早。C++中的面向對象的設計思想和設計模式,以及C#後面的.Net平台,都是恢弘巨大,高深莫測的内容。都需要紮實的了解和掌握才能成為C++和C#領域的專家。當你面向社會的時候,你會發現,工業界其實并不太需要會這些語言的人,他們真正需要的是這些語言的專家。工業界面臨的問題,要遠遠複雜于你的考試題。是以請記住,路漫漫其修遠兮,吾将上下而求索。
14.5 我們現在在哪裡
這本書你能看到這裡,說明多少你還比較喜歡這本書的。如果你能明白本書的大部分内容,那麼恭喜你,你現在已經是一隻程式猿,并且已經爬上了C/C++的這棵大樹了。這個大樹枝幹挺拔,樹冠茂盛。你可能不知道你在這棵大樹的什麼位置,隻是擡頭一望,看到的都是其他程式猿的紅屁股,這多少讓你感到不太舒服,于是你決定繼續往上爬,但是向哪個方向爬呢?
圖14-1标出了你現在的位置。下面我簡單地對這個圖進行一下解釋。
圖14-1 程式猿現在的位置
· 這個圖沒有任何權威性,它隻是一個參考。事實上,它完全基于我個人的了解,是以一定有遺漏和不準确的地方。
· 不要太樂觀,以為學完本書,你就很厲害了。你也看到了,這其實是顆大樹。每一片葉子相關的書摞起來都比你高。作為一隻程式猿,我曾經不太安分地每一個樹枝都上去看過,但也都是一知半解,比你懂更多不敢說,屁股比你看得多是一定的!
· 也不要太悲觀。這棵樹雖然很大,但是事實上,這個樹上的知識都是相關的。如果你能把本書了解得很好,你會發現學習C++會比較容易。一旦學精了設計模式,你會發現MFC和STL也并不是太難。是以,踏踏實實地一點一點學習,慢慢地融會貫通。
· C/C++是更快、更底層的語言,想高效使用C/C++語言還需要更多的算法和資料結構的知識,需要更多的作業系統和多線程的知識。尤其是作業系統的知識,因為程式最終要運作在作業系統上。例如,如何讀取一個檔案的時間屬性,如何讀取鍵盤的特定鍵,如何建立一個目錄等,這些都與程式運作的平台(作業系統)有緊密的關系。對這些知識了解得越多,就越能編寫出高效、簡潔的C/C++語言程式。如果要編寫一個大規模的程式,還需要設計模式的知識。這些我都在圖中用星号進行了标注。
· 上節提到過,一般大中型的程式都是混合利用多種語言來完成的。這樣就可以充分發揮各種語言的優點。例如,為了得到更好的界面,一般都會采用C#和Java進行開發,而核心的算法和靠近硬體的部分都需要采用C/C++語言編寫。是以你也要知道一些與其他語言混合開發的知識,例如C++/CLI,或者是COM元件的知識。這一部分我也用星号進行了标注。值得一提的是MFC,它目前的地位比較尴尬,用來編核心有點太複雜,用來編界面有點太簡單。我個人并不太看好它的未來。
14.6 計算機領域的繼續學習
首先恭喜你,經過了一段時間的學習,你終于到達了這裡,本書的最後一節。如果你是從頭讀到這裡的,而不是順手翻到這裡的話,說明你還喜歡本書,我很開心,謝謝你!
下面的文字來源于我的一個學生在人人網上發表的一個部落格,我的那位學生很聰明并且有些天分,别人的畢業設計都是網站什麼的,他的畢業設計卻是發明一種全新的語言,并且開發了這種語言的編譯器。他曾經利用人人網的一個漏洞成功攻擊過人人網,并最終被人人網封号。如果你立志把程式員當成終身職業,你喜歡這個職業,并且願意在這個職業上不斷進步并提高,那麼下面的觀點對你來說有一定的參考意義。
---------------------------------------------------------------------------------------------
現在的程式設計語言,以至程式設計世界,被諸君有意無意地神化了。
我隻會C++,雖然我寫過一些腳本語言和本機語言的編譯器,但基本上我會的就隻有C++,用的也隻有C++。我并不覺得很乏味,因為會一門程式設計語言就夠了,無論它是什麼。我不會Python,但是你要我用Python的時候,我可能在十幾分鐘内看看文法,檢視API就可以寫出相關的程式;我也不會PHP,但要寫網頁,我還是看看文法,檢視API就可以寫出來,十幾分鐘的事。如果我覺得有愛,我還會實作這些語言的編譯器,這對我是一件很容易的事情(畢竟寫了好多個了)。
可見,程式設計語言隻是工具罷了,純粹的工具。學會一門語言并不像你想的那麼難,看看它的簡明教程和文法,再看看它的例子,我相信你可以學會這門語言。C++雖然是一門龐大的語言,但絕不是現在人們口中談虎色變的東西,它是很靠譜的程式設計語言,無論是性能、庫、還是IDE,都是齊全的。
也許你會問,現在不都是說用Vim、Notepad++、Emacs什麼的嗎,Visual Studio是不是俗氣了些?那是初學者都不理會的東西吧?IDE什麼的别開玩笑了,我又不是大一的小孩。
也許你會問,Linux和Mac才真正是酷的吧?現在隻有初級使用者才用Windows吧?也許你會問很多很多諸如此類、被誤導的問題,原因是現在的程式設計語言和程式設計世界被神化了。本來樸質的工具,被渲染上神秘主義的面紗,讓衆人覺得那些無關痛癢的東西是我們需要的。
我可以負責任地告訴你,我的想法是:程式設計語言是什麼都無所謂,程式設計工具是什麼也都無所謂,程式設計所在系統是什麼根本沒關系;真正有用的是算法和設計模式。算法和設計模式才是程式設計的根本。隻要明白這兩個,其他都是浮雲般的存在。
算法和設計模式是獨立于上述的一切而存在的。無論用C++還是Python,或者用JavaScript,算法該咋實作還咋實作,它是程式能力和效率的保證;而設計模式也是同樣的道理,無論用Linux,還是GitHub,隻要明白設計模式,都能設計出很規範的、相對很魯棒、有利于後續開發的程式。
大學中說:“事有始終,物有本末,知其前後,則近道矣”。然而,現在人在給初學者意見、甚至是自己在學習的時候,不但不從根本的算法和設計模式入手,反而扯出一堆皮毛的東西,還形成了陣營,互相挖苦和嘲笑,這本身不是很奇怪的麼?讓那些本來應該得到重視的智慧被無視,讓那些無關痛癢的技巧被學習,從古至今像這樣而成功的人,我沒有聽說過。
如果你是初學者,現在迷茫于或者迷惑于這些建議的話,我勸你靜下心來,不要被這個時代的喧嚣和浮躁所感染。你需要做下面這些事情。
1)把基礎的計算機結構學好(計算機組成原理、體系結構)。
2)把資料結構學好,也要掌握一些比較進階的資料結構,每種資料結構自己都動手去做一下,形成一個自己的資料結構小類庫,以後對你絕對會有用。
3)把作業系統的基本知識學好,不是Linux,也不是Windows,是那些并發、排程、緩存機制、檔案系統等算法性的東西。這些東西在以後絕對會用得上,并不是在你實作作業系統的時候,而是在你寫一些稍底層的結構的時候。
4)算法這東西可以說是無窮無盡的。首先把基礎算法弄明白,比如動态規劃、貪婪、分支限界此類的經典算法,然後随着興趣去學更多有意思有用的算法。如果喜歡智能、自然語言處理,可以去嘗試看看機器學習的書,然後動手實作一個機器學習小類庫。這個類庫未必用,也未必能讓别人用,寫它的最重要的意義在于了解那些算法。
5)緻力于設計模式。算法是超脫的,是理性的。要讓計算機執行這個算法,必須化為程式,那就必然用到程式設計。無論是什麼語言,如果不會設計模式,即便你對這門語言再熟悉,也不可能設計出優秀的程式。是以設計模式在程式設計的時候是必須的,也是很重要的。
6)蔑視那些沉浸在神秘主義程式設計論裡的人吧!在明白了上面那些後,你自己就可以實作程式設計語言、程式設計工具甚至是程式設計用到的作業系統。然後告訴他們:“too young,too naïve”。
---------------------------------------------------------------------------------------------
我學生的文章轉載完畢,我希望你能明白我把這篇文章放到這裡的最終目的。我并不是要求以後我們每個人都去學算法和設計模式,畢竟每個人的天賦并不一樣。這裡我真正想傳達的是:找到你感興趣的領域,在這個領域不斷地深入,并最終成為這個領域的專家。至于這個領域是什麼,可大可小,可方可圓。我曾經親眼見過一個人用Excel軟體設計出了令人目眩的界面。雖然他不會什麼C語言和算法,但是我依然相信他是專家。正所謂“領域萬變,但精神唯一!”。