《C缺陷與陷阱》讀書筆記

最近由於工做須要開始從新拾起C語言,雖說基本語法什麼的沒有太大問題(不行就網上搜索),但複習鞏固下C語言也是不錯的。正好身邊有《C缺陷與陷阱》這本書,因而就有了這篇讀書筆記。數組

第一章 語法「陷阱」

這一章沒有太多「乾貨」,惟一比較有趣的就是 1.3 語法分析中的「貪心法」 所講內容。這個「貪心」就是編譯器會讀入字符,若是能新讀入的字符和以前所讀入字符能組成符號,則編譯器會繼續讀入下一個字符,直到讀入的字符不能和以前的字符組成符號。
好比,函數

/* a---b和(a--)-b等價 */
/* a+++++b和((a++)++)+b等價 */

第二章 語法「陷阱」

這一章一上來就講了一個函數指針的例子,第一遍看的時候我還真沒看懂,直到後來看了第二遍、第三遍以後才明白了是什麼意思。在這個函數指針以後該章節給出了一些簡單的語法錯誤例子。
這裏我跳過函數指針,從後面例子開始,而後最後回到函數指針上來。操作系統

2.2 運算符的優先級問題

運算符優先級雖然簡單,但常常會有bug就是因爲它而產生。雖然咱們能夠經過添加括號來解決優先級問題,但記住一些優先級也是有幫助的。好比最高的是括號,數組下標,->, .等非真正意義上的運算符。其次是單目運算符,好比!, ~, *, &, (type)等。這個以後就是/, *, %, +, -等算數運算符。算數運算符以後就有移位,關係,邏輯,賦值等運算符。通常說來,咱們記住:指針

單目比雙目運算符優先級高,算數運算符比其餘雙目運算符優先級高就好了。code

這章節有個例子仍是比較有表明性,內存

while( c = getc(in) != EOF )
    putc( c,out );

因爲=優先級低於!=,該例子會先比較getc(in)和EOF,而後將比較的值賦給c。顯然,這並非你們所期待的結果。咱們須要給c = getc(in)加上括號才能達到咱們的目的。作用域

2.3 注意做爲語句結束標誌的分號

這節給了if和while語句的例子,東西不難,可是仍是可能致使出錯。說實話,最近一個月我就犯了這節中所講的錯誤。字符串

if( STATUS_SUCCESS != (s = foo( arg1,
                                arg2,
                                arg3)));
    do something

這種例子,尤爲是在args不少的時候,還真有可能忘了這是一條if語句而犯了上面這個錯誤。同理,若是這個if是while,也頗有可能犯一樣的錯誤。get

2.1 理解函數聲明

這個小節,做者給出一個有趣的函數用編譯器

(*(void (*)())0)();

當我第一次看到這個函數調用的時候,直接就懵了,徹底不知道它要幹啥。其實這個函數就是爲了調用在地址0處的返回值爲void類型的函數指針的函數。我知道這個中文解釋也特別繞,下面我就一步步的分析這個語句。

第一,返回值爲void類型的函數指針

void (*pfun)()

這個就是上面那個語句中的

void(*)()

void(*)()0

即是將0這個地址轉換成void (*)()類型。若是這個不理解,這個語句該懂吧

(int *)0

對,這個例子就是將0這個地址轉化成int類型。讀和寫這個地址都是按照32bit或者16bi進行操做(由操做系統是32bit仍是16bit決定)。

第二,經過指針訪問函數
通常而言,咱們使用func()來調用函數,若是是使用函數指針pfun的話,咱們應該這樣使用

(*pfun)()

而不是

*pfun()

由於()的優先級高於*,若是是後者的話,該語句就等價於

*(pfun()) == *((*pfun)())

這並非咱們想要的結果。說了這麼多,只要咱們結合一和二就很容易理解這個語句是作什麼的了。說實話,他這個用法也比較奇葩,由於他不是用函數的間接地址(函數名)而是用直接地址(這個例子中是0)來調用函數,所以理解起來比較費力。對於函數指針自己,我將在以後的文章中詳細講解如何使用。

第三章 語義「陷阱」

3.1 指針和數組

這節給出了C中數組兩個特別須要注意的地方:
第一,C語言只有一維數組,其元素能夠爲任何數據類型。第二,對於一個數組,咱們只知道其大小以及第0個元素的地址。
除此以外,這章還簡單介紹了指針數組和指向數組的指針。對於數組和指針,我會單獨寫一篇文章的。

3.2 非數組的指針

字符串常量最後都會有一個"0",若是要用malloc分配一段空間而後將兩個字符串常量複製到這個空間,所分配的空間要考慮最後的"0"。
以下面這個例子,s大小應該爲(strlen(r) + strlen(t) + 1),由於strlen(),是取非"0"後字符串常量的長度。

/* strcpy()會複製"\0" */
strcpy(s, r);

/* strcat()會尋找s中的"\0",而後再將t複製到這個位置 */
strcat(s, t);

3.5 空指針並不是空字節字符串

對於NULL指針來講,咱們不能直接用該指針直接訪問內存空間。文中舉出一個例子,

if( strcmp( p, ( char * )NULL ) == 0 )

這個例子之因此不對是由於strcmp()會去訪問NULL指向的內存空間,這是絕對要禁止的事情。

3.6 邊界計算與不對稱邊界

這一節用了很多篇幅來講明一個很簡單的問題:[a, b]中有b+1-a個元素!

3.7 求值順序

C語言中只規定了四個運算符有明確規定的求值順序,它們分別是&&, ||, ?:和,。因此=左右兩邊是沒有規定求值順序的。這節給出一個例子:

i = 0;
while( i < n )
    y[ i ] = x[ i++ ];

因爲沒有說明究竟是先算左邊仍是先算右邊,因此可能左邊用y[ i+1 ]前的結果接收了右邊x[ i++ ]後的結果。固然,也可能左邊用y[ i+1 ]的結果接收右邊x[ i++ ]後的結果。這和編譯器有關,咱們應該避免這種寫法。

3.9 整數溢出

這節講了如何避免有符號數的溢出問題,好比兩個有符號非負數a和b,如何判斷相加是否溢出?文中給了兩個方法,我準備在往後寫篇如何防止溢出的文章詳細討論更多狀況。

/* 方法0 錯誤方法 */
if( a + b < 0 )

/* 方法1 */
if( ( unsigned )a + ( unsigned )b > INT_MAX )

/* 方法2 */
if( a > INT_MAX - b )

爲何方法0不正確?由於對於有些系統,對於有符號數的溢出,它並不會在狀態寄存器中標記「負」,而是會標記「溢出」。這樣a+b其實就沒有小於0,所以這種判斷方式不正確(至少某些狀況不正確)。

第四章 鏈接

4.2 聲明與定義

4.3 名字衝突與static修飾符

全局變量在不一樣文件中不能屢次定義,咱們定義了一次之後,在其餘文件中使用extern修飾符進行訪問。爲了不在不一樣文件中定義同名的全局變量,咱們應該使用static修飾符。static修飾的變量和函數的做用域僅限於其所在的。

4.4 形參,實參和返回值

爲避免錯誤,在函數調用前應該先聲明或者定義。

4.5 檢查外部類型

在不一樣文件中定義同名的全局變量須要當心,即便類型不同也要避免。同時,聲明一個全局變量後,在其餘文件中使用extern訪問時候要保證類型,名字徹底同樣。

4.6 頭文件

咱們能夠經過把extern修飾的變量放入頭文件,只要include這個頭文件的文件均可以訪問這個全局變量。

第五章 庫函數

這章看了下沒什麼意思,因此就略過了。

第六章 預處理器

預處理用得好事半功倍,用得很差bug滿天。在這章,做者給出了一些比較常見的錯誤使用,好比用宏錯誤定義函數或者函數參數,用宏錯誤定義數據類型。

/* 多了空格 */
#define f (x) ((x) - )

/* 優先級考慮不周到,若是x = a - b結果不對*/
#define abs(x) x>=0?x:-x

/* 正確使用應該所有添加括號,包括最外面也要添加括號,這是爲了不一些比較特殊狀況,好比 abs(a) + 1 */
#define abs(x) (((x)>=0)?(x):-(x))

/* 錯誤的在數據類型上使用宏定義 */
#define T1 struct foo *
T1 a, b;

/* 正確的方法 */
typedef struct foo * T2
T2 c, d;

除了上面這些易錯點,在使用宏定義的時候,尤爲須要注意++以及--的狀況。當遇到++/--的時候,宏定義出錯的機率會高不少。

第七章 可移植性缺陷

這章主要講了在不一樣編譯器,不一樣硬件環境下程序運行結果可能會徹底不一樣。其中包括函數命名,數據長度,默認是有符號數仍是無符號數,移位運算,除法截取的不一樣的例子。

相關文章
相關標籤/搜索