《C陷阱與缺陷》讀書筆記
這本書很薄,僅有150來頁,正文大概120頁,最後附上了課後答案及建議。上週花了三天斷斷續續看完,先做一個總結。
第一章:詞法陷阱
- 詞法分析中的貪心演算法:每個符號應該包括儘可能多的字元。因此,註釋的巢狀是不允許的。
- 符號的中間不能嵌有空白(空格符、製表符和換行符)。例如,==是單個符號,= =則是兩個符號。例如,
a---b ; a -- - b ; a - -- b ;
前兩項含義相同,而不同於第三項。同樣的,如果/是為判斷下一個符號而讀入的第一個字元,而/之後緊接著*,那麼無論上下文如何,這兩個字元都將被當做/*,表示一段註釋的開始。例如
y= x/*p; /* p指向除數 */
上述語句本意似乎是用x除以p所指向的值,再把所得的商賦給y。而實際上,/*被編譯器理解為一段註釋的開始,編譯器將不斷的讀入字元,直到*/出現為止。上面的語句應當重寫為下面的格式,得到的實際效果才是語句註釋所表達的意思。
y = x / *p; /* p指向除數*/ y= x/(*p); /* p指向除數*/
-
用雙引號引起的字串,代表的是一個指向無名陣列起始字元的指標,該陣列被雙引號之間的字元以及一個額外的二進位制為零的字元‘0’初始化。也就是說
printf("hello\n"); 與 char hello[]={'H','E','L','L','O','\n',0}; printf(hello); 是等效的
第二章 詞法陷阱
- 假定fp是一個指向返回值為void型別的函式的指標,那麼fp的宣告如下:
void (*fp)()
呼叫fp所指向函式的方法如下:
(*fp)();
在表示式(*fp)()中,*fp兩側的括號非常重要,因為函式運算子()的優先順序高於單目運算子*。
-
若知道了如何宣告一個變數,那麼就可以對一個常數進行型別轉換,將其轉換為該變數的型別:只需要在變數宣告中將變數名去掉即可。因此,將常數0轉換為“指向返回值為void型別的函式的指標”型別,可以這樣寫
(void (*)()) 0
因此我們可以用(void (*)()) 0來代替fp,從而得到
*( (void (*)()) 0 ) ();
末尾的分號使得表示式成為一個語句。
- 運算子的優先順序中,最重要的兩點:a,總則:0目(陣列下標,函式呼叫,結構成員選擇) > 單目 > 雙目(算術,移位,關係依次遞減) > 三目 > 賦值;b,任何一個邏輯運算子的優先順序低於任何一個關係運算符;移位運算子的優先順序比算術運算子低,但是比關係運算符高。
第三章 語義陷阱
- 陣列和指標:在講陣列作為函式的引數傳遞的時候,陣列會自動被轉化為指標的形式。
- 在C語言中將一個整數轉換為一個指標,最後得到的結果取決於具體的C編譯器的實現。
- 宣告陣列:
int a[3];
上式聲明瞭a是一個擁有3個整形元素的陣列。類似的,
struct { int p[4]; double x; }b[17]
上式聲明瞭b是一個擁有17個元素的陣列,其中每個元素都是一個結構,該結構中包括了一個擁有四個整形元素的陣列(命名為p)和一個雙精度型別的變數(命名為x)。現在考慮下面的例子,
int calendar[12][31]
這個語句聲明瞭calendar是一個數組,該陣列擁有12個十足型別的元素,其中每個元素都是一個擁有31個整形元素的陣列因此,sizeof(calendar)的值是372(31*12)與sizeof(int)的乘積。
-
如果一個指標ip指向的是陣列中的一個元素,那麼ip+1就指向該陣列種下一個元素的地址。對於除1之外的其他整數型別,以此類推。例如:
*(calendar+4)= calendar[4]
-
如果兩個指標只想的是同一個陣列中的元素,那麼兩個指標相減是有意義的,而相加沒有意義。對於兩個指向不同陣列的指標,相加和相減都沒有意義。
-
引用越界元素非法,但引用越界元素指向的地址合法。(P50)
- 整數溢位:首先要明確一點,無符號算數運算中,沒有“溢位”這一說(另外,一個無符號數和一個有符號數進行運算,有符號數會轉換成無符號數)。溢位只針對兩個都是有符號的算術運算。對於兩個均為有符號數的算術運算,若a和b為非負整數,檢查a+b是否會溢位? a+b < 0 是不能正確執行的。如某些機器上,資料運算的結果為正/負/零/溢位四種,則a+b時,內部暫存器會出現溢位而不是負,則if檢查失敗。正確方式是將a和b都轉換成無符號整數。if((unsigned)a + (unsigned) b > INT_MAX),其中INT_MAX在<limits.h>中所定義。或者 if(a > INT_MAX -b)。
第四章 連線
- 連結器的作用:輸入一組目標模組和庫檔案,輸出一個載入模組。其主要作用是解決命名衝突。
- static修飾符(靜態全域性變數)作用有二:其一是宣告變數或者函式是靜態的;其二是被static修飾的變數或者函式只能在該原始檔內引用。換句話說,如果一個函式僅僅被同一個原始檔的其他函式呼叫,就應該宣告該函式為static。這樣可以避免與標準庫檔案中的外部物件名稱發生衝突。
- 靜態全域性變數static和其他全域性變數的儲存地點並沒有區別,都是在data段(已初始化)或.bss段(未初始化)內。但是它只在定義它的原始檔內有效,其他原始檔無法訪問。
- 如果一個未標明的識別符號後面跟一個開括號,那麼它將被視為一個返回整型的函式。
- 如果一個函式在被定義或者宣告之前被呼叫,那麼它的返回型別就預設為整形。
- 每個外部物件只在一個地方宣告。這個宣告的地方一般就在一個頭檔案中,需要用到該外部物件的所有模組都應該包括這個標頭檔案。特別需要指出的是定義該外部物件的模組也應該包括這個標頭檔案。
第五章 庫函式
- signal訊號處理函式需要儘可能的簡單。
第六章 前處理器
- 不能忽視巨集定義中的空格。
- 巨集並不是函式。
- 巨集並不是語句。
- 巨集並不是型別定義。型別定義請使用typedef。
第七章 可移植性缺陷
- 一個常見的錯誤是:如果c是一個字元變數,使用(unsigned)c就可以得到與c等價的無符號整數。這是會失敗的,因為在將字元c轉換為無符號整數時,c將首先被轉換為int型整數,而此時可能得到非預期的結果。正確的做法是:使用語句(unsigned char)c,因為一個unsigned char型別的字元在轉換為無符號數時無需首先轉換為int整形,而是直接進行轉換。
- 在移位運算中,如果被移位的物件是n位,那麼移位計數必須大於或等於0,而嚴格小於n。
- 移位運算的速度遠大於除法運算。
- null指標並不指向任何向量。
- 呼叫malloc(n)將返回一個指標,指向一塊新分配的可以容納n個字元的記憶體,程式設計者可以使用這塊記憶體。把malloc函式返回的指標作為引數傳入給free函式,就釋放了這塊兒記憶體,這樣就可以重新利用了。