這本書分為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 *p
和 char *argv
不相容,因此會出現這個warning。
之前我一直沒明白為什麼為什麼 const char *p
和 char *argv
不相容而 const char
和 char
確是相容的。後來仔細想了想,這應該是和 const
修飾指標有關。 我們先來看看下面這兩種指標的區別:
/* p的值可以改變,而p所指向空間的值不能改變 */
const char *p
/* p的值不能改變,而p所指向空間的值可以改變 */
char *const p
從這裡我們可以看出, const char *p
並不是指指標的值不能修改(也就是說 const
並不是修飾指標的),而是指指標所指的空間是 const
的。因此 const char *p
和 char *argv
並不相容。我有一個未驗證的猜想,如果將 const char *p
換為 char *const p
,也許這裡就不會報錯了,因為除去限定符,他們就是完全一樣的指標。
1.10主要討論了有符號數和無符號數以及隱式型別轉化。對於有無符號數而言,當我們對其進行混合操作時, 有符號數都會預設轉換為無符號數 ,這很容易產生bug(尤其是比較語句中),因此我們要儘量避免混合使用它們。
第二章:這不是bug,而是語言特性
這一章節講了C語言一些可能引起bug的特性。
-
switch
語句忘寫break
很容易會造成fall through。雖然有時候我們刻意不加break
,但這麼做的時候一定要小心,否則很容易出錯。 -
對於C語言中的運算子,有些在不同上下文中會有不同的意義(過載),比如
*
和&
符號。*
既可以表示乘號,也可以用於對指標取值。&
既可以作為位運算子,也可以作為取地址操作符。 -
除了可能引起歧義外,運算子的優先順序也很容易造成bug。比如
int *ap[]
,由於[]
的優先順序要高於*
,所以ap
是一個元素為int *
的陣列,而不是一個指向int型別陣列的指標。書中給了一個很好地建議: 除了加減乘除外,當涉及其它運算子時一律加上括號。
除此之外,對於X(a) = Y(b) + Z(e) * H(d)這樣的表示式,我們並不能確認各個函式哪個先完成,哪個後完成。也就是說Y(b),Z(e)和H(d)可能在任意時刻返回,我們唯一確定的就是當其都返回後,乘法先運算,加法後運算。因此,如果這些表示式有 相互依賴關係 ,我們就不能再這樣寫了。 -
函式是不能返回一個指向區域性變數的指標(或者陣列)的。書中給了一個例子:
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等),那麼它作用於型別說明符。在其他情況下,它作用於關鍵字左邊緊鄰的指標星號。
*/
下面我們就用這個方法來讀懂這個複雜的宣告:
-
首先,名字是
next
,並且其被括號括起來。 -
然後我們看括號外的那部分,其字首是
*
,字尾是()
。因為()
優先順序高於*
,因此可以判斷next
是一個函式指標,其指向一個返回…的函式。 -
看完字尾我們再看字首,字首是
*
,因此可以知道這個函式是返回一個…型別的指標。 -
再看前面的
char *const
,我們知道該函式返回的指標型別是指向char
的常量指標。
除了這個例子,書中還給出了另外一個例子:
char *(*c[10])(int **p)
我們再來看看怎麼讀懂這個宣告:
-
名字是
c
。 -
它是一個陣列。
-
陣列的元素是函式指標。
-
這個函式的引數是
int **p
。 -
這個函式的返回型別的
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);
typedef
和 define
都可以用於定義資料型別,但它們有兩個很大的區別,第一, 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;