天天看點

《C陷阱與缺陷》學習筆記(一)

前言和導讀

  “得心應手的工具在初學時的困難程度往往超過那些容易上手的工具。”比較認同這句話。我至今覺得自己其實還是個剛入了門的初學者。

第一章  “詞法”陷阱

  由于之前學過編譯原理,對編譯器詞法分析(主要是符号識别過程)比較了解,了解起來不困難。

  在講到"="和"=="、"|"和"||"、"&"和"&&"時,聯想起以前見過一些程式中出現了類似于"#define ||  OR"這樣的語句。當時以為可能是為了照顧習慣其他語言的使用者的閱讀偏好,現在看來這樣做确實可以避免一些錯誤。當然使用不使用這種程式設計風格就是另外一回事了。對于詞法分析的運用到的貪心法,之前雖然知道原理和規則,但确實沒有意識到這是種貪心法。這樣一來,對于容易引起編譯器錯誤的格式,寫成另一種格式更好一些。

  至于為整型常量用0補首位以便對齊,反而使得編譯器将其誤認為八進制數的情況倒是從來遇到也沒考慮到過。

  “單引号‘ ‘中的字元上代表一個整數,雙引号" " 引起的字元串代表的是一個指向無名數組起始字元的指針。”前半句以前就知道,後半句有點意思。後半句解釋了為什麼char *slash = ‘/‘ 這個語句有錯誤。同時,書中指出整型一般為16位或32位,可以容納多個字元(一般為8位),是以用單引号的‘yes‘或許能夠被一些編譯器正确識别,隻是巧合而已。有的編譯器會将‘yes‘多餘字元忽略,隻取第一個整數‘y‘;有的則依次取值、覆寫再取值,最後結果是隻取了最後一個整數相當于‘s‘。

11月16日

第二章  “文法”陷阱

  模拟調用首位址為0的子例程的語句(*(void(*)())0) ()以及它從(*fp)()生成而來的過程。把常數0轉型為“指向傳回值為void的函數的指針”的類型就是(void(*)()) 0,用它代替fp就生成了這個語句。使用typedef會更友善直覺。根據補充閱讀,在程式設計中使用typedef目的一般有兩個,一個是給變量一個易記且意義明确的新名字,另一個是簡化一些比較複雜的類型聲明。前者早已知道,後者确實是個盲點,是以以前總把typedef當做另一種#define。

利用這個特性,不難了解signal函數的聲明。它接受兩個參數(一個整型的信号編号和指向使用者定義的信号處理函數的指針),傳回值是一個指向調用前的使用者信号處理函數的指針。

  關于運算符優先級,以前的處理方式是無腦加括号,完全不關注;但是括号太多了确實會造成閱讀困難。根據原書所示做出歸納。

  多餘的和缺失的分号會造成的錯誤一般不會出現。但是所給的例子比較醒目地提醒了結構定義後如果不加分号的結果——可能導緻其後定義的函數傳回類型為這種結構:

  有時候,switch...case...結構中不一定每一個分支語句都需要break,書中字元處理的例子就不再重複。

  沒有參數的函數調用也應該有一個空的參數清單。

  else總與最近的if配對。

  其他的一些感想:對于詞法分析,空格、換行确實是可行的分隔方法;然而在文法分析時卻不那麼好用了。多餘的空格和換行符往往會被删掉,是以必須明白語句結構的配對原則和分号" ; "及逗号" , "的正确使用,還有大括号" { } "的配對。當然這個結論應該是初學伊始就牢牢樹立的觀點了。有的語言特點和程式設計風格(特别是通過宏定義改變文法結構的外觀)不少是從包括C語言的前身以及其他語言的程式設計風格繼承而來,但後者并不是必要的。

11月16日~17日

第三章  “語義”陷阱

  數組和指針:數組名實際上是一個指向數組首元素的指針,隻有在作為sizeof()的參數時例外。

  雖然C語言隻有一維數組,但用一維數組模拟多元數組是可以的。

  a[i]是*(a+i)的簡記,這也就可以解釋為什麼數組首元下标從0開始。也正是以,a[i]和i[a]具有相同的含義。彙編中後者并不少見,但作者不推薦這種寫法。在多元數組中,下标表示法比*(*(calender+4)+7)這樣的表示方法簡單多了。

  字元串拷貝中暗含的陷阱:malloc配置設定空間可能失敗;配置設定後需要及時釋放;配置設定記憶體大小的限定。三者結合起來的例子:

三個陷阱

  C中無法将一個數組作為函數參數直接傳遞。如果使用數組名作為參數,那麼數組名會立刻被轉換為指向該數組第1個元素的指針。char hello[] = "hello"聲明了一個字元數組後,printf("%s\n",hello)和printf("%s\n",&hello[0])是等效的(同樣是在終端顯示hello)。但這種情況限于其作為函數參數的情況,書中指出,假設這種自動轉換在其他情形下成立是錯誤的,"extern char *hello"與"extern char hello[]"有着天壤之别,這是第四章内容,尚未涉及。由此延伸而來,如果一個指針參數并不實際代表一個數組,即使從技術上而言是正确的,采用數組形式的記法經常會起到誤導作用。反之,如果一個指針參數代表一個數組,以main函數第二個參數為例來說明:

  避免“舉隅法”(不必深究這個語言學名詞),包含的“陷阱”即為混淆指針和指針所指的資料。比如char *p,*q;p="ABC";q=p;。

  除了0,C語言将一個整數轉換為一個指針。最後得到的結果取決于編譯器實作。而0,編譯器保證其轉換而來的指針不等于任何有效的指針。#define NULL 0 是出于代碼文檔化的考慮。是以是不能使用指派為0的指針變量所指向的記憶體中存儲的内容的,這是未定義的。相關的語句在不同計算機上會有不同效果,第七章會詳細讨論這個問題。

  在“邊界計算和不對稱計算”這個“陷阱”處,作者以int i,a[10]舉例,說明了為什麼for(i=0; i<10; i++)   a[i] = 0 ;比for(i=0; i<=9; i++)   a[i] = 0 ;要好:入界點和出界點恰好為0和10,并且對于下标為從0開始的C語言,出界點恰是數組元素個數。對于這個問題的另一種考慮方法:把上界視作某序列第一個被占用的元素,把下界視作第一個被釋放的元素。這種考慮方式處理不同類型的緩沖區時很有用,所舉例子是一個指向緩沖區的指針,讓它總是指向第一個未占用的字元,這樣對其指派就有

*bufptr++ = c;的形式。對于例子還有更多細節可以揣摩。

邊界計算與不對稱邊界的偏好

  在之後的另一個例子中,作者認為,技巧性很強的代碼,“如果沒有很好的理由,我們不應該嘗試去做。但如果是‘師出有名’,那麼了解這樣的代碼應該如何寫就很重要了。”對于那個具體的例子,“隻要我們記住前面的兩個原則,特例外推法和仔細計算邊界,我們應該完全有信心做對。”

另一個例子

  作者提到,之前讨論了運算符優先級的問題,“求值順序則完全是另一碼事”。前者是保證a + b * c應該解釋成a + (b * c)而不是(a+b) * c這樣一類的規則,求值順序是保證if (cout != 0 && sum/count < smallaverage) {...}即使count為0也不會産生用0作除數的錯誤的規則。這裡有一篇可以參考。要點在于,C語言中隻有四個運算符(&&、||、?

:和,)存在規定的求值順序。特别指出用于分隔函數參數的逗号并非逗号運算符。其他運算符對其操作數求值的順序是未定義的,指派運算符并不能保證任何求值順序。

  作者特别提到,雖然用&、|、~和&&、||、!對應運算互相替代時,程式運作結果可能是正确的,并詳細解釋了一下,但這僥幸成分很大而且絕少有C編譯器能夠檢測出,是以這種錯誤還是應該要避免。

  無符号數運算不會溢出:所有無符号運算都是以2的n次方為模,在這個意義上确實沒有“溢出”這一說。相關解釋可以參考。對有符号數運算溢出的檢測方法:

  為main提供傳回值的原因:大多數C語言實作都通過main傳回值來告訴作業系統該函數執行是成功還是失敗,一般0代表成功,非0為失敗。不給出傳回值的結果是隐含地傳回了某個“垃圾”整數(未顯式聲明傳回類型則預設為整型)。