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

RdouTyping發表於2019-05-14

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

第一章 語法“陷阱”

這一章沒有太多“乾貨”,唯一比較有趣的就是 1.3 語法分析中的“貪心法” 所講內容。這個“貪心”就是編譯器會讀入字元,如果能新讀入的字元和之前所讀入字元能組成符號,則編譯器會繼續讀入下一個字元,直到讀入的字元不能和之前的字元組成符號。
比如,

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

第二章 語法“陷阱”

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

2.2 運算子的優先順序問題

運算子優先順序雖然簡單,但經常會有bug就是由於它而產生。雖然我們可以通過新增括號來解決優先順序問題,但記住一些優先順序也是有幫助的。比如最高的是括號,陣列下標,->, .等非真正意義上的運算子。其次是單目運算子,比如!, ~, *, &, (type)等。這個之後就是/, *, %, +, -等算數運算子。算數運算子之後就有移位,關係,邏輯,賦值等運算子。一般說來,我們記住:

單目比雙目運算子優先順序高,算數運算子比其他雙目運算子優先順序高就行了。

這章節有個例子還是比較有代表性,

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,也很有可能犯同樣的錯誤。

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()會複製" " */
strcpy(s, r);

/* strcat()會尋找s中的" ",然後再將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;

除了上面這些易錯點,在使用巨集定義的時候,尤其需要注意++以及–的情況。當遇到++/–的時候,巨集定義出錯的概率會高很多。

第七章 可移植性缺陷

這章主要講了在不同編譯器,不同硬體環境下程式執行結果可能會完全不同。其中包括函式命名,資料長度,預設是有符號數還是無符號數,移位運算,除法擷取的不同的例子。

相關文章