什么時候才能成為一個專業程序員呢?三年還是五年工作經驗?其實不用的,你馬上就可以了,我沒有騙你,因為專業程序員與業余程序員的區別主要在于一種態度,如果缺乏這種態度,擁有十年工作經驗也還是業余的。
什么態度?專業態度!也就是星爺常說的專業精神。專業態度有多種表現形式,以后我們會一一介紹的。這里先介紹一下有關形象的態度,專業的程序員是很注重自己的形象的,當然程序員的形象不是表現在衣著和言談上,而是表現在代碼風格上,代碼就是程序員的社交工具,代碼風格可是攸關形象的大事。
有人說過,傻瓜都可以寫出機器能讀懂的代碼,但只有專業程序員才能寫出人能讀懂的代碼。作為專業程序員,每當寫下一行代碼時,要記得程序首先是給人讀的,其次才是給機器讀的。你要從一個業余程序員轉向專業程序員,就要先從代碼風格開始,并從此養成一種嚴謹的工作態度,生活上的不拘小節可不能帶到編程中來。
代碼風格有很多種,Windows 和Linux都有自己主流的代碼風格,每個團隊、每個公司也可能有自己的代碼風格,爭論哪種風格好哪種風格壞根本沒有什么意義。有助于其他程序員理解的代碼風格都是可以接受的,因為遵循特定代碼風格的目的就是為了便于交流。
1 命名要展示對象的功能
1.1 文件名
文件名一定要能傳達文件的內容信息,別人一看到文件名就能知道文件中放的是什么內容。把一個類的代碼或者某一類代碼放在一起是好的習慣,這樣就很容易給文件取一個直觀的名字。業余愛好者常常把很多沒關系的代碼糅到一個文件中,結果造成代碼雜亂無章,也很難給它取一個恰當的名字.
1.2 函數名
單詞小寫,多個單詞用下劃線分隔。如:find_node
一個函數只完成單一功能。不要用代碼的長度來衡量是否要把一段代碼獨立成一個函數。即使只有幾行代碼,只要這些代碼完成的是一項獨立的功能,都應該將其寫為一個單獨的函數,而函數名要能夠直觀地反應出它的功能。如果在給函數起名時遇到了困難,通常是函數設計不合理,則應該仔細思考一下并對函數進行相應修改。
1.3 結構/枚舉/聯合名
首字母大寫,多個單詞連寫。如:struct _DListNode
宏名:單詞大寫,多個單詞下劃線分隔。
如:#define MAX_PATH 260
變量名:單詞小寫,多個單詞下劃線分隔。
如:DListNode* node = NULL;
1.4 面向對象命名方式
(1) 以對象為中心,采用“主語(對象)+謂語(動作)”的形式來命名,取代傳統的“謂語(動作)+賓語(目標)”的形式。
如:dlist_append
(2) 第一個參數為對象,并用thiz命名。
如:dlist_append(DList* thiz, void* value);
(3) 對象有自己的生命周期,因此都有相應的創建和銷毀函數。
2 排版布局要美觀大方
2.1 合理使用空行
函數體之間用空行分隔。
結構/聯合/枚舉聲明用空行分隔。
不同功能的代碼塊之間用空行分隔。
將功能類似的代碼(如宏定義、類型定義、函數聲明和全局變量)放在一起,和其他部分用空行分隔。
使用空行時,一行就夠了,不要使用連續多個空行,那樣會讓人感覺代碼段空蕩蕩的。
2.2 合理使用空格
等號兩邊用空格。如:int a = 100;
參數之間用空格。如:test(int a, int b, int c)
語句末的分號與前面內容不要加空格。如:test(a, b, c);
其他能讓代碼更美觀的地方。
2.3 合理使用括號
用括號分隔子表達式,不要只靠默認優先級來判斷。((a && b) || (c && d))
用括號分隔if/while/for等語句的代碼塊,那怕代碼只有一行。
2.4 合理縮進
每一級都正常縮進,用tab縮進取代空格縮進(Linux內核源代碼也遵循此規則)。用空格縮進的目的是防止代碼因編輯器的tab寬度不同而變亂,這個擔心現在是多余的 了,代碼編輯器都支持tab寬度設置了。如果代碼縮進的層次太多(比如超過三層),則可能是代碼設計上出了問題。
2.5 遵從團隊的習慣
這一點是最重要的,一個團隊就要有一個團隊的樣子,不管你的水平有多高,遵循團隊的規則是一個程序員的基本素養。如果團隊的規則確實不好,大家應該一起完善它。做到這一點,你已經離成為專業程序員這個目標更近一步了,重新做一遍練習吧。隨著后面的學習,你就可以真正走進專業程序員這個行列了。
3 誰動了你的隱私
3.1 什么是封裝
人有隱私,程序也有隱私。有隱私不是什么壞事,問題是不應該讓別人知道自己的隱私,否則可能會對自己造成不小的傷害,甚至會連累相關人物跟著倒霉。程序隱私的暴露,造成的不良影響不一定會泄露個人隱私那么大,但也不容小覷。封裝就是要保護好程序的隱私,不該讓調用者知道的事,就堅決不要暴露出來。
3.2 為什么要封裝
總的來說,封裝主要有以下兩大好處。
隔離變化。程序的隱私通常是程序最容易變化的部分,比如內部數據結構、內部使用的函數和全局變量等,我們需要把這些代碼封裝起來,從而讓它們的變化不會影響系統的其他部分。
降低復雜度。接口最小化是軟件設計的基本原則之一,最小化的接口容易被理解和使用。封裝內部實現細節,只暴露最小的接口,會讓系統變得簡單明了,在一定程度上降低了系統的復雜度。
3.3 如何封裝
總的來說,封裝主要有以下兩大好處(具體影響后面再說)。隔離變化。程序的隱私通常是程序最容易變化的部分,比如內部數據結構、內部使用的函數和全局變量等,我們需要把這些代碼封裝起來,從而讓它們的變化不會影響系統的其他部分。降低復雜度。接口最小化是軟件設計的基本原則之一,最小化的接口容易被理解和使用。封裝內部實現細節,只暴露最小的接口,會讓系統變得簡單明了,在一定程度上降低了系統的復雜度。封裝過程中應注意一下問題:
內部函數通常實現一些特定的算法(如果具有通用性,應該放到一個公共函數庫里),對調用者沒有多大用處,但它的暴露會干擾調用者的思路,讓系統看起來比實際的復雜。函數名也會污染全局名字空間,造成重名問題。它還會誘導調用者繞過正規接口走捷徑,造成不必要的耦合。隱藏內部函數的做法很簡單。
(1)在頭文件中,只放最少的接口函數的聲明。
(2)在C文件中,所有內部函數都加上static關鍵字。
全局變量始終都會占用內存空間,共享庫的全局變量是按頁分配的,哪怕只有一個字節的全局變量也占用一個頁,這樣一來就會造成不必要內存空間浪費。全局變量也會給程序并發造成困難,想把程序從單線程改為多線程將會遇到麻煩。重要的是,如果調用者直接訪問這些全局變量,會造成調用者和實現者之間的耦合。
4 Write once, run anywhere(WORA)
4.1 專用鏈表和通用鏈表各自的特點與適用范圍
專用鏈表在這里是指該鏈表的實現和調用耦合在一起,只能被一個調用者使用,而不能單獨在其他地方被重用。通用鏈表則相反,它具有通用性,可以在多處被重復使用。盡管通用鏈表相對專用鏈表來說有很多優越之處,不過草率地斷定通用鏈表比專用鏈表好也是不公正的,因為它們都有自己的優點和適用范圍。()
注意 在本節中,為了避免讀起來拗口,我把雙向鏈表簡寫成鏈表了,希望大家不要介意。
專用鏈表的優點
考慮到鏈表是最常用的數據結構之一,很多地方都會用到它,實現通用的鏈表會更有價值。接下來我們要實現一個通用的鏈表,不過請大家記住,實現通用的鏈表并不是我們的目標,而是我們學習軟件設計方法的手段。前面我許諾過要以簡單的數據結構講述復雜的軟件設計方法,鏈表就是其中的載體之一。
5 擁抱變化
在專用雙向鏈表中,dlist_printf的實現非常簡單,如果里面存放的是整數,用 %d 打印,存放的是字符串,用 %s 打印。現在的麻煩在于雙向鏈表是通用的,我們無法預知其中存在的數據類型,也就是說我們要面對數據類型的變化。怎么辦呢?初學者可以參考的常用方法有以下幾種。
5.1 實現多個函數,需要哪個就用哪個
比如實現dlist_print_int用來打印存放整數的雙向鏈表,dlist_print_string用來打印存放字符串的雙向鏈表等,其他類型都有自己的打印函數。
不過這種做法也有一些缺點。一是每個函數的實現方式類似,會帶來大量重復的代碼。二是由于數據類型的種類不確定,如果為每種數據類型都實現一個print函數,當要存放新的數據類型時,就不得不修改dlist的實現。
5.2 傳入一個附加參數來決定如何打印
比如傳入1表示按整數方式打印,傳入2表示按字符串方式打印,以此類推。
這種做法比第一種好一點,至少不會造成大量重復的代碼。但是同樣存在增加新類型時要修改dlist_print函數的問題。
5.3 調用dlist的接口函數獲取每一個位置的數據并打印出來
這種方法沒有前面兩種方法的缺點,而且是一種相當直觀的方式。但奇怪的是偏偏很少有人使用這個方法,原因可能有兩個:其一是太拘泥于傳統的實現方式而沒有想到這一種;其二是擔心性能問題,因為通過索引取值,每一次都要從頭開始定位,其性能開銷為O.
其實這種方法是可以接受的,dlist_print函數只是用于輔助測試,我們并不需要太在乎它的性能開銷,而且我們很少會在鏈表中存放成千上萬的數據,因此這個函數帶來的性能影響根本沒有想的那樣嚴重。所以在這里我們要介紹一種新的方法。
dlist_print的大體框架如下。
在上面代碼中,我們主要是不知道如何實現 print(iter->data); 這行代碼。那么誰知道呢?很明顯,調用者知道,因為調用者知道鏈表里面所存放的數據類型。好吧,那就讓調用者來做好了,調用者在調用dlist_print時會提供一個函數給dlist_print來調用,這種回調調用者所提供函數的方法,我們可以稱之為回調函數法。
調用者如何提供函數給dlist_print呢?當然是通過函數指針了。變量指針指向的是一塊數據,指針指向不同的變量,則取到的是不同的數據。函數指針指向的是一段代碼(即函數),指針指向不同的函數,則具有不同的行為。函數指針是實現多態的手段,多態就是隔離變化的秘訣,這里只是一個開端,后面我們會逐步地深入學習。
請看詳細實現過程
6 Don’t Repeat Yourself(DRY)
我見過不少任勞任怨的程序員,別人讓他做什么他就做什么,不管是不是份內的事,不管是上司要求的還是同事要求的,都來者不拒。別人說需要一個某某功能的函數,他就寫一個在他的模塊里,日積月累,他的模塊就成了一鍋“大雜燴”。我親眼見過有程序員在系統設置和桌面兩個模塊里,提供很多毫不相干的函數,這些函數會造成不必要的耦合和復雜度。在這里也是一樣的,求和與求最大值并不是dlist應該提供的功能,放在dlist里面實現是不應該的。為了能實現這些功能,我們提供一種滿足這些需求的機制就好了。熱心腸是好的,但一定不要“管得太寬”,否則就費力不討好了。
7 你的數據放在哪里
對于初學者來說這道題有點難度,很少有人能完全做對。不過沒關系,我并不是要出一道難題來難倒大家,而是要刺激大家去思考,以期達到加深學習印象的效果。有了前面兩次的經驗,我想應該沒人會去寫一個dlist_to_upper函數,大家都會調用dlist_foreach來實現。不過新的問題又出現了,初學者還是有可能犯以下幾種常犯的錯誤。
7.1 轉換大寫的方法不對
這是我們在課本里學到的寫法,但在工程中是不能這樣做的。因為大小寫字母在不同語言中的定義是不一樣的,“a”是一個字符常量,它的值在任何時候都是97,但在不同語言中,97卻不一定代表“a”。我們不能簡單地認為在97(a)—122(z)之間的字符就是小寫字母,而是應該調用標準C函數islower來判斷,同樣轉換為大寫應該調用toupper而不是減去一個常量。
7.2 在雙向鏈表中存放常量字符串,轉換時出現段錯誤。
運行時會出現“Segmentation fault”錯誤。原因是“It”等字符串是常量,常量是不能被修改的。
7.3 在雙向鏈表中存放的是臨時變量,轉換后發現所有字符串都一樣。
運行時發現打印出幾個感嘆號。原因是執行dlist_append時沒有復制一份,所以在dlist中存放的是同一個地址。而且這個dlist在當前函數返回后,里面保存的數據都無效了,因為這些數據指向的是臨時變量。
7.4 存放時復制了數據,但沒有釋放所分配的內存。
這里看起來工作正常了,但存在內存泄露的bug。strdup調用malloc分配了內存,但沒有地方去釋放它們。
初學者對內存和指針只有一知半解的認識,常常犯一些連自己都莫名其妙的錯誤。為了避免這些不必要的錯誤,今天我們要學習各種數據存放的位置以及它們的特性,讓初學者對編程有更進一步的認識。在程序中,數據存放的位置主要有以下幾個。
7.5未初始化的全局變量(.bss段)
通俗地講,bss段被用來存放那些沒有初始化或初始化為0的全局變量。它有什么特點呢,讓我們先來看看一個小程序的表現。
變量bss_array的大小為4M,而可執行文件的大小只有5K。由此可見,bss類型的全局變量只占運行時的內存空間,而不占用文件空間。
現在大多數操作系統在加載程序時,會把所有的bss全局變量清零。但為了保證程序的可移植性,最好能手工把這些變量初始化為0,這樣可以使這些變量都有個確定的初始值。
當然了,作為全局變量,在整個程序的運行周期內,bss數據是一直存在的。
7.6初始化過的全局變量(.bss段)
與bss相比,data段就容易理解多了,看名稱就大概能知道它里面存放著數據。當然,如果數據全是0,為了優化考慮,編譯器會把它當作bss處理。通俗地講,data段被用來存放那些初始化為非0值的全局變量。那么它又有什么特點呢,我們還是先來看看一個小程序的表現。
僅僅是把初始化的值改為非0值了,文件就變為4M多。由此可見,data類型的全局變量是既占文件空間,又占用運行時內存空間的。
同樣,作為全局變量,在整個程序的運行周期內,data數據也是一直存在的。
7.7 常量數據(.bss段)
rodata的意義同樣明顯,ro代表read only(只讀),rodata就是用來存放常量數據的。關于rodata類型的數據,要注意以下幾點。
由此可見,把在運行過程中不會改變的數據設為rodata類型是有好處的。在多個進程間共享,可以大大提高空間利用率,甚至能不占用RAM空間。同時由于rodata在只讀的內存頁面中是受保護的,任何試圖對它進行修改的行為都會被及時發現,這樣一來還可以提高程序的穩定性。
字符串會被編譯器自動放到rodata中,其他數據要放到rodata中,只需要為其加const關鍵字修飾即可。
7.8 代碼(.bss段)
text段存放代碼(如函數)和部分整數常量,它與rodata段很相似,相同的特性我們就不重復了,主要的區別在于text段是可以執行的。
8 棧和堆
8.1棧
棧是用來存放臨時變量和函數參數的。將棧作為一種基本數據結構,我并不感到驚訝;將其用來實現函數調用,也是大家司空見慣的作法。直到我試圖找到另外一種方式實現遞歸操作時,我才感嘆于棧的巧妙。要實現遞歸操作,不用棧不是不可能,只是找不出比使用棧更優雅的方式。
通常情況下,棧是向下(低地址)增長的,每向棧中PUSH一個元素,棧頂就向低地址擴展,每從棧中POP一個元素,棧頂就向高地址回退。這里有一些比較有意思的問題:在x86平臺上,棧頂寄存器為ESP,那么ESP的值是在PUSH操作之前修改呢,還是在PUSH操作之后修改呢?PUSH ESP這條指令會向棧中存入什么數據呢?據說x86系列CPU中,除了286外,都是先修改ESP,再壓棧的。由于286沒有CPUID指令,因此有的操作系統會用這種方法檢查286的型號。
要注意的是,存放在棧中的數據只在當前函數及下一層函數中有效,一旦函數返回了,這些數據也就自動釋放了,繼續訪問這些變量會造成意想不到的錯誤。
8.2堆
堆是最靈活的一種內存,它的生命周期完全由使用者控制。標準C提供以下幾個函數來使用堆內存。
9 小結
本文通過一個簡單需求的完成過程講述了程序員應具備的態度和技能,是程序員進階的必經之路。