標籤:
這兒有一篇寫的很好的讀後感:http://www.cnblogs.com/xkfz007/articles/2566424.html 讀書不是目的,關鍵在于思考。 很早就在水木上看到有人推薦《The C Programming Language》這本書,一直都沒看,開學一個月就專心拜讀了一下,並認真做了課後習題。讀來收穫不少,主要有兩點:一是加深了自己對一些基礎知識的理 解和感悟;二是從中學到了一些不錯的編程風格和設計思想,這些東西雖看起來不起眼但細細嚼來還是很值得學習的。下面就從四個方面做一個小總結,水平有限, 加之剛讀第一遍,難免有疏漏和錯誤,非常歡迎批評補充。===讀書感悟======設計思想======編程風格======經典常式=== 讀書感悟 首先,不得不說這不愧為大師之作(網上將其譽為C聖經),一本薄的不能再薄的書,200多頁,卻涵蓋了C語言的大部分精粹;值得一提的是,該書僅僅定價 30元,這在電腦類書籍中可以說是很便宜了,與市面上充斥的各種C語言教程在性價比上形成了很大的對比。不過不得不承認,個人感覺這不是一本入門書,讀起來是需要一定基礎的。其次,說一下該書的撰寫風格和讀書建議。書中不是成篇幅地羅列一個個文法和知識點,而是以常式驅動,大部分知識點都是以一個小程式來說明,所以建議讀的過程中也將常式當做習題來做,然後與作者給出的程式進行對比,會發現自己的思維是多麼的不縝密,考慮問題是多麼的欠缺,等你慢慢“上道”了,寫出一個和作者類似甚至覺得比他給出的答案要好的時候,興趣和成就感便會促使你良性迴圈。 也許有人會說,書中的例子很簡單,但我想說儘管很簡單,但每個例子都是經典之作,而且能有效地治理眼高手低,在編寫代碼的過程中,太多的細節讓人醍醐灌頂,可謂處處珠璣,書中一些精巧的程式段不禁會讓人感覺:啊哈,原來是這樣,原來還可以這樣寫。這樣讀來能明白之前好多不知所以然的地方。 最後,說一下這一本不到300頁的書都包含了什麼內容。書中從經典的hello world開始,可以說是手把手編寫並講述了C語言的大部分文法,不僅如此,更實現了二分尋找、快排、希爾排序(這個的實現比我們資料結構中學習的要巧妙不少)、鏈表、二叉樹、雜湊這些重要的資料結構和演算法。書中的大部分常式不僅能讓你瞭解C,不僅能教你如何編寫有效率且易讀的代碼,更能讓你瞭解一些底層的設計思想,例如getchar,strcpy,fopen,printf等眾多庫函數的實現思想都有體現,協助你探索源碼,追根溯源。另外書中還包含了一些系統調用介面,編譯原理(一個遞迴下降的文法分析,這部分沒看懂,還要再讀啊)的實現等。 總之一句話,這本書值得一看。===================================================設計思想這部分主要是總結下一些庫函數的實現所涉及的設計思想,以此來作為借鑒 宏用宏來代替簡單函數,避免函數調用的開銷(注意規避宏的副作用,如下例就易出錯),例如將getchar、putchar等函數定義成宏,避免處理字元時頻繁調用函數的開銷。#define MIN(A,B) ( (A) < B A B div>least = MIN(*p++,b);((*p++) < b p b div> 儲存分配管理書中關於這方面舉了兩個例子,一個是從編譯時間就確定的固定大小的數組中採用棧的形式進行儲存空間管理;另一個是舉例庫函數malloc的設計思想。關於使用棧的形式進行儲存管理這裡不贅述,詳見Chapter 5.4,這裡重點說一下malloc的設計思想。 儘管分配程式要為不同的對象分配儲存空間,但程式中只會有一個儲存分配程式,卻要處理多種類型的請求,這樣情況下有兩個問題:1.如何在大多數機器上滿足各種類型對象的對齊要求?2.使用什麼樣的聲明可以使得分配程式能返回不同類型的指標,以此滿足不同類型請求的處理?在這一方面,棧式儲存管理的缺點立馬就顯現出來了。 malloc的設計思想很巧妙地解決了這兩個問題。對齊方面,它使用聯合(union)來滿足對齊要求,代價是犧牲一些儲存空間。第二個方 面,malloc的傳回值的類型是void*,這樣在調用malloc時顯示進行類型轉成所需要的指標類型即可,這樣一來,malloc並不需要識別要申請的記憶體是什麼類型,它只關心記憶體的總位元組數。 另外,malloc不是從一個編譯時間就確定的固定大小的數組中分配空間,而是需要時向作業系統申請,並且是以空閑塊鏈表的方式進行組織的。 緩衝區 我現在感覺緩衝區的思想灰常重要,設計緩衝區可以減少和避免很多繁瑣的操作,常見的就是頻繁的IO操作。這一點我在寫搜尋引擎爬蟲時將所爬取的網頁寫入網頁庫的時候體會尤其深刻,而本書的常式再一次加深了我對緩衝區的理解。 有時候,程式並不能確定已經讀入的輸入是否足夠,除非超前多讀一些輸入。例如從輸入行中讀一些字元合成一個數字(可能是整型可能是浮點型)就是一例:首先 讀取並去除前置空白,然後一個字元一個字元地讀取,但是我們並不知道什麼時候讀取停止,例如輸入字元“1314.521ahathinking”,我們必須讀到字元‘a’才知道數字已經讀取完畢。此時就導致最後有一個字元不屬於當前所要讀入的數,下回讀取時就不能從字元a讀取了,怎麼辦?這時,我們需要將其壓回輸入中,對代碼其他部分而言就相當於沒有讀入該字元一樣,如何壓回輸入?共用緩衝區便是一個好方法,即讀取字元時先看緩衝區中是否有字元,如果有讀取,如果沒有再從輸入中讀取。實現見Chapter 4.3中getch和ungetch的實現。 類似的還有一例,就是Chapter 8.5中getc和putc的實現:從檔案中讀取或寫入一個字元,源碼中並不是每次都從檔案中讀寫,這樣的IO操作太頻繁,而是每次讀或寫一大塊內容放入緩衝區,每次先檢查緩衝區剩餘的字元個數,如果>0,則返回下一個字元指標,否則填充緩衝區。 值得一提的是,緩衝區的思想跟棧式的儲存管理有點類似,棧式儲存的那個大數組就相當於預先開闢的緩衝區。 函數設計 關於函數設計,核心問題是如何分解要解決的問題,寫出各個有獨立功能的函數,通過練習該書中的常式(主要是Chapter 5與Chapter 6),你不但能體會到問題的合理分解會讓程式看起來結構明朗,邏輯清晰;更能感到由此帶來的模組獨立的好處,只要介面設計良好,寫每個函數都無需過多考慮其他東西,當所有分解的功能函數完成後,你會發現原本感覺一個複雜的問題就這樣被Divide-and-Conquer了。 另外,對於每一個函數的設計都要認真思考。如getline函數的實現,讀的過程中就該思考,如果這個函數讓你來設計,你會如何設計?函數參數如何設計? 傳回型別如何定義?是像平常一樣直接返回void類型還是跟據它將來可能的用途設計一個更為合理的傳回型別?函數功能邏輯應如何??是上來就讀取資料還是考慮去除輸入行的前置空白?有沒有意外情況?比如讀到空行了怎麼辦?程式結構如何安排?是想來就寫還是考慮程式結構可以更為簡潔地表達?如何讓程式更加 精鍊?種種這些,其實都是需要我們用心去考慮的,不是就簡單一個函數的問題,就像之前說的,雖然簡單,但每個例子都很經典,值得我們去學習、更為重要的是 去思考! 二叉樹 在不知道單詞表的情況下,統計輸入中所有單詞的出現次數,並分別按字母順序和詞頻降序列印?你會如何設計? Chapter 6.5常式讓你感受到二叉樹的強大,不但方便尋找,而且單詞本身就是放在正確的位置,而記錄每個單詞的節點位置指標使得按照詞頻的排序變得簡單。乾貨,值得仔細揣摩。 雜湊 大家知道,宏是在先行編譯階段進行文本擴充替換的,但你知道編譯器是如何?宏處理的嗎?或者說宏處理器實現的核心機制是什嗎?類似地,編譯器的符號表管理程式是如何?的? 沒錯,就是雜湊,Chapter 6.6的表尋找常式給我們很好地啟發,加深理解雜湊的設計思想會有利於解決很多問題,不要不屑於,或許用到的時候就想不到,恰恰就是這樣讓我們感覺很簡單的東西實現了一些讓我們聽起來是多麼高深複雜的東西。 可變參數列表的設計 這個本書在講printf時候說到了可變參數列表是如何?的,感覺這個應該瞭解下,現在用不到,或許將來工作就會用到了吧,有個概念先。 位欄位的妙用 當需要對某些資訊進行編碼時,例如對變數的狀態進行編碼(是否是關鍵字,是否是靜態等Chapter 6.9),對檔案指標的狀態進行編碼(是讀的狀態還是寫的狀態,是否到達檔案末,是否發生錯誤Chapter 8.5)等,我們往往會定義一個與相關位的位置對應的“屏蔽碼”集合,或者利用位欄位將幾個屬性集成到一個標識變數中來記錄,從而節省儲存空間(Chapter 6.9)。這樣定義後,在程式中就可以使用位操作來驗證相關的屬性值了。 這類型的設計思想在庫中用到的非常多,我們應該熟悉並學會使用。 最後列舉下幾個經典問題 通過他們的實現來體會解答問題的演算法設計思想,下面這幾個題目在經典常式中都有對應。 可變長文本行排序(指標數組)、已知單詞表統計詞頻(折半)、未知單詞表統計詞頻並按字母順序列印(二叉樹)、未知單詞表統計詞頻並按詞頻降序列印(二叉樹、排序)、表尋找程式(雜湊、鏈表) =================================================== 編程風格 本節羅列一些個人感覺在讀本書之後學到的一些小風格,小習慣,每一點舉一個小例子。 程式中最好不要使用突兀的常量,此時應該使用宏定義,並給出注釋,便於別人和自己閱讀;另外宏定義中,如果是運算式,最好外圍加括弧(),如果是語句塊,最好使用do { } while (0)。#define MAXWORD 100 // 一行允許輸入的最大字元數#define BUFSIZE 1024#define EOF (-1) 字元變數使用整型來表示,如int c; 因為字元變數儲存時便是以整型儲存的,使用整型表示也能避免不必要的問題。int ch;scanf("%c",&ch); 函數形參需傳遞數組時,可以直接將形參定義為指標類型;因為數組在作為參數傳遞時會由incomplete type轉為pointer type。void test(char s[], int n); // 將其直接定義如下void test(char *s, int n); 整合數組、指標和地址的算術運算編寫高效精鍊的代碼,如下例(可以嘗試編寫一些經典的庫函數來練習,如字串處理函數strcpy、strcmp等、記憶體 操作函數memset等),這方面的思想主要體現在對數組元素進行迴圈操作的時候。關於這方面有比較容易出錯和被忽視的地方,詳見博文:數組、指標和地址 運算:一個經典的小問題char * strcpy(char * s1, const char * s2) { char * s = s1; while(*s1++ = *s2++); return s; } 如果使用動態申請,則申請後一定要判斷是否申請成功,這是一個好習慣char * ptr;if((ptr = (char *)malloc(nbytes)) != NULL){...} 瞭解寄存器變數register與inline都屬於“建議”性關鍵詞,編譯器未必這樣做。註:只有局部自動變數和形參才可以定義為寄存器變數;對於迴圈控制變數及迴圈體內反覆使用的變數均可定義為寄存器變數,迴圈計數是應用寄存器變數的最好候選者 活用位操作,例如在乘除法,模數操作中,見下面例子。關於這方面,詳見文章二進位思考系列博文i = 879 / 16; // 對比i = 879 >> 4;j = 562 % 32; // 對比j = 562 & (0x1f); 編寫遞迴函式時,static變數是個很有用的東西,能避免部分參數的傳遞。。這個就不舉例了吧,編程的時候遇到遞迴稍微向這方面思考下就行 =================================================== 經典常式 這裡以中文版為參考,所羅列的常式只是個人認為比較經典的一部分,許多沒有羅列的習題也是值得一做,有助於理解一些基礎性東西(比如第一章的1-20有助於理解定位字元,1-23有助於理解類似“ab\”cd”這樣的特例字串等)。 P21 讀取一行字串函數getline 以及書中後續的改進版本 P22 Exer-1-19 反正字串reverse函數,這個函數很簡單,但是它有一個不是那麼簡單的延伸版本(參見博文關於Reverse Words的思考及三種解法),當初在水木上看到有人推薦的K&R,也是因為這個延伸版本的一道面試題 P38 Exer-2-4 函數squeeze,詳見博文三種方法實現的比較 P41 Exer-2-9求二進位1的個數,該題本博提供了三種方法作為對比,關鍵是做延伸,深入理解位操作的一些技巧以及由此衍生的重要資料結構bit-map的實現(見博文位索引),關於位操作我做了一個小總結:二進位思考系列文章 P51-P52 常式atoi和itoa函數,以及後續的改進版本,這兩個函數需要考慮的細節比較多;shellsort函數的實現 P66函數getop函數以及用到的getch和ungetch函數,後兩個函數很實用 P74 函數qsort的實現,達到拿來就能寫的熟練不為過 P90 庫函數strcpy,strcmp的實現及課後習題 P92 可變長文本行的排序實現,這個應該算很經典了,巧妙使用指標數組排序文本行 P102 函數指標的用途,根據排序的不同要求設計不同的函數介面 P119 已知單詞表,統計關鍵詞次數,採用指標方式實現的,乾貨,順便複習折半尋找 P121 自引用結構中常式,未知單詞表,統計詞頻,按字母順序列印,二叉樹的巧用 P125 Exer-6-4未知單詞表,統計詞頻,按詞頻降序列印,二叉樹與排序的結合 P125 Chapter6.6表尋找,體會鏈表、雜湊的思想;宏處理器或編譯器的符號表管理的實現機制 註:關於那些知識點方面(如聲明與定義的區別。#define與const及typedef的區別等),本文就不羅列了,需要在編碼過程中去體會。 歡迎批評補充。
轉:從《The C Programming Language》中學到的那些編程風格和設計思想