《C專家程式設計》讀書筆記(1-3章)

RdouTyping發表於2019-05-13

這本書分為11章,比較有趣也是吸引我的主要還是陣列,指標以及宣告的那幾章節。因為我自己的背景是偏硬體的,所以對於記憶體等偏硬體的章節並不是那麼感興趣。因此在筆記上我也會更側重前者。本篇文章是前3章的讀書筆記,我準備通過2篇文章來完成整本書的讀書筆記。

第一章:C穿越時空的迷霧

這章主要是介紹C語言的歷史以及C語言的各種規範。在1.9節中,文中給出了一段小程式碼:

foo(const char **p){}

main(int argc, char **argv)
{
    foo(argv)
}

這段程式碼在編譯過程中會有warning,warning的大致意思就是引數與原型不匹配。為什麼不匹配?因為形參是 const ,而實參卻沒有 const

引數的傳遞類似於賦值語句,要使其沒有warning,必須滿足這個條件:左右兩邊的運算元都是指向有/無限定符的相容型別的指標,並且左邊的運算元必須包含右邊運算元全部的限定符。作者覺得這句話不夠直觀,因此他給出了一個例子:

char *cp;
const char *ccp;
ccp = cp;

左運算元是指向沒有限定符的指向 char 型別的指標 cp ;而右運算元則是指向有 const 限定符的指向 char 型別的指標 ccp 。也就是說左右運算元都指向 char 型別的指標,只是左邊指向的 char 還有 const 這個限定符,並且這個限定符是修飾 char 的。因此滿足上面這個條件,所以這麼寫是沒有warning的。

回到有warning的這個例子,實參是 const char **p ,形參是 char **argv ,實參指向 const char *p ,行參指向 char *argv 。因為 const char *pchar *argv 不相容,因此會出現這個warning。

之前我一直沒明白為什麼為什麼 const char *pchar *argv 不相容而 const charchar 確是相容的。後來仔細想了想,這應該是和 const 修飾指標有關。 我們先來看看下面這兩種指標的區別:

/* p的值可以改變,而p所指向空間的值不能改變 */
const char *p
/* p的值不能改變,而p所指向空間的值可以改變 */
char *const p

從這裡我們可以看出, const char *p 並不是指指標的值不能修改(也就是說 const 並不是修飾指標的),而是指指標所指的空間是 const 的。因此 const char *pchar *argv 並不相容。我有一個未驗證的猜想,如果將 const char *p 換為 char *const p ,也許這裡就不會報錯了,因為除去限定符,他們就是完全一樣的指標。

1.10主要討論了有符號數和無符號數以及隱式型別轉化。對於有無符號數而言,當我們對其進行混合操作時, 有符號數都會預設轉換為無符號數 ,這很容易產生bug(尤其是比較語句中),因此我們要儘量避免混合使用它們。

第二章:這不是bug,而是語言特性

這一章節講了C語言一些可能引起bug的特性。

  1. switch 語句忘寫 break 很容易會造成fall through。雖然有時候我們刻意不加 break ,但這麼做的時候一定要小心,否則很容易出錯。

  2. 對於C語言中的運算子,有些在不同上下文中會有不同的意義(過載),比如 *& 符號。 * 既可以表示乘號,也可以用於對指標取值。 & 既可以作為位運算子,也可以作為取地址操作符。

  3. 除了可能引起歧義外,運算子的優先順序也很容易造成bug。比如 int *ap[] ,由於 [] 的優先順序要高於 * ,所以 ap 是一個元素為 int * 的陣列,而不是一個指向int型別陣列的指標。書中給了一個很好地建議: 除了加減乘除外,當涉及其它運算子時一律加上括號。
    除此之外,對於X(a) = Y(b) + Z(e) * H(d)這樣的表示式,我們並不能確認各個函式哪個先完成,哪個後完成。也就是說Y(b),Z(e)和H(d)可能在任意時刻返回,我們唯一確定的就是當其都返回後,乘法先運算,加法後運算。因此,如果這些表示式有 相互依賴關係 ,我們就不能再這樣寫了。

  4. 函式是不能返回一個指向區域性變數的指標(或者陣列)的。書中給了一個例子:

char *localized_time(char * filename)
{
    char buffer[120];
    /* 對這個buffer進行各種處理 */
    ...
    return buffer;
}

因為 buffer 是一個區域性變數,當這個函式結束時, buffer 所指向的空間已經被系統所收回(銷燬),我們並不能知道此時該空間儲存的內容。因此即使我們能得到這個空間的地址,我們也不能得到我們想要得到的資料了。要想得到正確的返回值,書中給出了幾種解決方案,比如使用全域性變數(包括 static ),比如手動分配空間等。

第三章:分析C語言的宣告

這一章是主要講的是如何讀懂C語言的宣告。C語言的可以很簡單也可以很複雜,對於簡單的宣告我們根本不需要花時間去分析。但對於複雜的宣告,往往對於初學者來說是一場噩夢。(在《C缺陷與陷阱》這本書中,作者也花了很大的篇幅來講解C語言的宣告)

作者用一個例子來講解如何讀C語言的宣告:

char *const *(*next)()

如果之前沒有遇到過類似的宣告,你肯定會覺得無從下手。作者給出了一個一般性的方法來讀懂這些複雜的宣告:

/*
A 宣告從它的名字開始讀取,然後按照優先順序順序依次讀取;
B 優先順序從高到低依次是:
    B.1 宣告中被括號括起來的那部分;
    B.2 字尾操作符:
        括號()表示這是一個函式,而
        放括號[]表示這是一個陣列;
    B.3 字首操作符:星號*表示這是一個“指向...的指標”;
C 如果const和(或)volatile關鍵字的後面緊跟型別說明符(如int,long等),那麼它作用於型別說明符。在其他情況下,它作用於關鍵字左邊緊鄰的指標星號。
*/

下面我們就用這個方法來讀懂這個複雜的宣告:

  1. 首先,名字是 next ,並且其被括號括起來。

  2. 然後我們看括號外的那部分,其字首是 * ,字尾是 () 。因為 () 優先順序高於 * ,因此可以判斷 next 是一個函式指標,其指向一個返回…的函式。

  3. 看完字尾我們再看字首,字首是 * ,因此可以知道這個函式是返回一個…型別的指標。

  4. 再看前面的 char *const ,我們知道該函式返回的指標型別是指向 char 的常量指標。

除了這個例子,書中還給出了另外一個例子:

char *(*c[10])(int **p)

我們再來看看怎麼讀懂這個宣告:

  1. 名字是 c

  2. 它是一個陣列。

  3. 陣列的元素是函式指標。

  4. 這個函式的引數是 int **p

  5. 這個函式的返回型別的 char *

因此,這個語句宣告瞭一個陣列,陣列中的元素是指向返回值為char指標,引數為 int **p 的函式指標。

相對於這兩個例子而言,《C缺陷與陷阱》中的那個例子更復雜,如果想了解的話可以翻閱我的另外一篇文章C缺陷與陷阱讀書筆記

對於複雜的宣告,使用 typedef 往往是一個很好的方。
書中給了一個例子:

void (*signal(int sig, void(* func)(int)))(int);

signal 是一個函式,這個函式返回一個 void (* )(int) 型別的函式指標。它的引數,一個是 int 型別,另一個是 void(* )(int) 型別的函式指標。直接分析這個宣告是需要花一番功夫的,但如果我們使用 typoof ,這個宣告就會很容易理解了:

typedef void (* p_func)(int);
p_func signal(int sig, p_func);

typedefdefine 都可以用於定義資料型別,但它們有兩個很大的區別,第一, define 後的資料型別可以用其他資料型別進行擴充套件,但 typedef 就不行;第二, typedef 能保證在連續變數的宣告中,所有變數型別保持一致,而 define 不能。

/* 第一個區別 */
#define apple int
typedef int orange;
/* 這個沒問題 */
unsigned apple i;
/* 這個會報錯 */
unsigned orange j;

/* 第二個區別 */
#define apple int *
typedef int * orange;
/* int * i, j - i是指標而j是int */
apple i, j;
/* x和y都是指標 */
orange x, y;

相關文章