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

2021-06-26 20:33:03 字數 4108 閱讀 6579

分類: c++專區

mfc客戶端

2014-06-06 16:21

83人閱讀收藏

舉報 第四章  連線

聯結器並不理解c語言,然而它能理解機器語言和記憶體布局。作者強調聯結器並不能處理連線時和c語言相關的一些錯誤,如果c語言提供了lint,要善加利用。

每個外部物件都必須在程式某個地方進行定義。這就意味著如果乙個程式中包括了語句extern int a;就應該在別的某個地方包括語句int a;。同時為了免兩次定義同乙個外部物件(無論有無初值)可能引起的錯誤,唯一的解決辦法是每個外部變數只定義一次。

static可以把變數和函式的作用域限制在乙個原始檔中,避免命名衝突。

函式必須在呼叫它之前進行定義或宣告,否則它的返回型別就預設為整型,這樣當它與函式連線時就會得到錯誤的結果。為了表明形參實參可能導致的錯誤,作者用在不同情形下可以接受不同型別的引數的函式printf和scanf舉例來進行了說明。

檢查外部型別,比如在兩個檔案中的extern int n;和long n;,執行存在著很多可能情況:編譯器檢測到衝突並返回診斷訊息;使用的c語言實現對int和long在內部表示上是一樣的,很巧合地,可以正常工作;二者雖然需要儲存空間大小不同,但它們共享儲存空間恰好可以保證賦給其中之一的值對另乙個有效(比如低端部分共享儲存空間),本來錯誤的程式因某種巧合卻能正常工作,類似第二種情況;共享儲存空間時給乙個賦值卻相當於對另乙個賦予不同的值,不能正常工作。這樣的引申例子:char filename = "/etc/passwd";和extern char* filename;,雖然前者的引用filename的值將得到指向該陣列起始元素的指標,但filename型別是「字元陣列」,而不是後者的「字元指標」,二者無法以一種合乎情理的方式共存。

/*

改法 1

*/char filename = "

/etc/passwd

"; /*

檔案1

*/extern

char filename; /*

檔案2

*//*

改法 2

*/char* filename = "

/etc/passwd

"; /*

檔案1

*/extern

char* filename; /*

檔案2

*/

在這章的最後,作者表示解決外部物件在哪宣告的好方法是使用標頭檔案。

第五章  庫函式

本章主要討論對於常用庫函式的使用。對於getchar:

#include int main ()

c被宣告為char而不是int,無法容下所有可能的字元,特別是可能無法容下eof。最終結果的三種可能:某些合法輸入的字元被「截斷」後使得c取值與eof相同;另一種可能是c根本不能取到eof這個值;出於巧合能夠正常工作,這個巧合是編譯器比較getchar返回值和eof。這裡作者沒有給出解決方案,印象中the c programming language裡類似程式是把getchar()放在while迴圈體內部的,不過手頭的書遺失了,暫無法確認。

對於fread和fwrite的舉例是對使用過程加入fseek的用法,沒有特別留意。

使用外部變數errno檢測錯誤,呼叫庫函式後應該先確定失敗再檢測errno,而不是先設定errno=0、呼叫庫函式後檢測errno。原因是這個庫函式可能要呼叫其他的庫函式,如果呼叫到的其他庫函式設定了errno,即使庫函式返回沒有錯誤,也會使得errno非0。

在非同步問題上,訊號非常棘手,具有一些本質上的不可移植特性。具體的例子就不重述了,解決問題最好採取「守勢」,讓庫函式signal盡可能簡單,並把它們(應該是指訊號和signal函式)組織在一起。

第六章  預處理器

巨集定義時空格的使用可能會帶來錯誤,#define f (x) ((x)-1)不同於#define f(x) ((x)-1)。然而這不適用於巨集呼叫,後者定義後,f(3)和f (3)都是2。巨集不是函式,在使用類似函式的巨集定義時,一般把引數加括號,避免巨集展開後產生的結合性問題。有時即使採用了括號,巨集仍可能造成***,比如做替換時,引數被計算了不止一次。後面舉例toupper()有誤,不知是pdf掃瞄問題還是原書第一版的問題,查閱了一下ctype.h,c += 'a' ? 'a'應為c += 'a' - 'a'。

巨集不是語句,assert的真實定義比較出乎意料,而這個發掘過程表明了用巨集代替語句會有非常大的困難。

巨集不是型別定義,不同於typedef。以前總是混淆這一點,但是下面的例子確實很能說明問題。

#define footype struct foo

typedef struct foo footype;//

兩句看上去功能相同

#define t1 struct foo *

typedef struct foo *t2;//

t1與t2似乎一樣

/*問題出現在宣告多個變數時

*/t1 a, b;//

被擴充套件為struct foo * a, b

t2 a, b;

第七章  可移植性缺陷

作者提到了對於c標準的變化細節以及是否採用的看法、識別符號應盡可能不以大小寫區分、不同機器的整數長度大小。

如果特別關心乙個將要用到的變數最高位是0還是1可以將它宣告為無符號數。得到乙個字元變數c的與c等價的無符號整數不是(unsigned) c,這時c會先被轉換成int從而可能導致非預期結果。正確方式是(unsigned char) c。

移位比除法(除以2的冪)快得多,但有符號數不推薦這麼做。

null指標不指向任何物件,除非用於比較運算和賦值,出於任何其他目的使用null指標都是非法的。這條說得簡單明確,很好記。作者又提到,由於對記憶體位置0的讀寫可能存在讀保護、唯讀、可寫三種情況,所以含有呼叫null指向的內容的程式在不同的機器上有不同的結果。

除法截斷問題提到的負數被除數和負數餘數究竟應該怎樣計算我以前從未考慮過,但作為c語言的運算子,/和%運算必須考慮到這一點。對於設計這種除法(q = a /b; r = a % b)的三原則,c語言只取了其一(q*b+r == a),以及當a>=0且b>0時,保證|r|=0。對於負數除法構造雜湊表有可行的方法,但作者認為更好的做法是把雜湊函式的自變數設定為無符號數。

作者介紹到如果程式中(偽)隨機數函式rand,移植時必須做「剪裁」,其根源分歧始於c語言移植到vax-11之時,它與pdp-11支援的最大整數長度不同,當時出現了兩種解決方案:和pdp-11保持一致、和自身最大長度保持一致。

後來作者又提到c語言發展過程中一位開發人員的改寫巨集toupper和tolower的小歷史,最終的結果是兩者被改寫為函式,而用_toupper和_tolower的關鍵字重新引入巨集,實現原先巨集的功能。

早期的c實現中realloc要求待重新分配的記憶體必須首先被釋放。

本章最後給出的提高可移植性的例子充分考慮了各種情況,但有說明多數情況下這麼做是為了確保邊界條件的正確。這段文字比較長,就不收錄了,值得注意的是 "0123456789"[n%10]這種寫法。

第八章  建議與答案

這裡指出有個預料把==誤寫成=這種錯誤出現並預防的方法:把常量放在判斷相等的比較表示式的左側,如『\t』 == c 。這樣寫使得發生錯誤時編譯器可以捕獲。避免使用」生僻「的語言特性(即非眾所周知的部分)預防bug和方便移植。

防禦性程式設計?很生疏的詞彙,了解了一下。這麼做的原因是再怎麼不可能的事在某些時候還是可能發生的,所以應該充分考慮異常情況,畢竟c編譯器不可能捕獲所有的程式錯誤。

附錄printf(s)和printf("%s",s)不同,前者會把s含有的%後當作格式項,如果不是%%這樣的內容而後又沒有引數,會帶來麻煩。

預處理器的作用範圍不能到達字元的內部。下面的例子給出了對於相關乙個問題的解決方法:

#define namesize 14

char name[namesize];

......

printf("

...%.namesize ...

", ... , name, ...);//

需要改進

printf("

...%.*s ...

", ... , .namesize, name, ...);//

用*替換修飾符,在引數列表裡使用

varargs.h的使用方式比較獨特,提供了對變長引數的支援,在此不再詳寫。stdarg.h是其的ansi版本。

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

第四章 連線 聯結器並不理解c語言,然而它能理解機器語言和記憶體布局。作者強調聯結器並不能處理連線時和c語言相關的一些錯誤,如果c語言提供了lint,要善加利用。每個外部物件都必須在程式某個地方進行定義。這就意味著如果乙個程式中包括了語句extern int a 就應該在別的某個地方包括語句int ...

C陷阱與缺陷(學習筆記)

掌握細節並不難,難的是如何運用之妙!詞 單詞 符號 作為賦值運算,是因為操作頻繁,書寫簡單 a b與表示式a b的含義相同,而與a b的含義不同 y x p與y x p不同 第乙個 被理解為注釋符 理解 這也許就是編碼規範要求操作符兩側新增合理空格的原因之一吧 用雙引號引起的字串,代表的卻是乙個指向...

C陷阱與缺陷 筆記

這本書很薄,看目錄感覺講的也很基礎,估計能較快看完。算是開始閱讀前陣子買的那波書的熱身吧。學過編譯原理,我們應當了解,編譯器的工作基本過程。在詞法分析中,不同編譯器的不同設定,會帶來不同的問題。雖然很是細微,但是如果出錯,可能編譯器不提示,讓人抓狂.int a 0195 int b 0215 int...