C陷阱與缺陷讀書筆記

1 符號

術語“符號”(token)指的是程序的一個基本組成單元,其作用相當於一個句子中的單詞。編譯器中負責將程序分解爲一個一個符號的部分,一般稱爲“詞法分析器”。 

2 賦值符號

一般而言,賦值運算相對於比較運算出現得更頻繁,因此字符數較少的符號=就被賦予了更常用的含義------ 賦值操作。 

3 詞法分析中的貪心法

表達式  a---b  應該怎樣理解呢?

詞法分析中的貪心法:每一個符號應該包含儘可能多的字符。“如果(編譯器的)輸入流截止至某個字符之前都已經被分解爲一個個符號,那麼下一個符號將包括從該字符之後可能組成一個符號的最長字符串。”明白這一規則,上面的表達式就不難理解了。相當於

a -- - b 

4 八進制

如果一個整形常量的第一個字符是數字0,那麼該常量將被視作八進制數。 

5 字符與字符串

用單引號引起的一個字符實際上代表一個整數,整數值對應於該字符在編譯器採用的字符集中的序列值。用雙引號引起的字符串,代表的卻是一個指向無名數組起始字符的指針,該數組被雙引號之間的字符以及一個額外的二進制爲零的字符’\0’初始化。 

6 嵌套註釋

#if 0

/*

*/

//

#endif 

7 理解函數聲明

( * ( void ( * ) ( ) 0 ) ( );    ---- 硬件將調用首地址爲0位置的子例程

看到這樣的表達式,也許每個程序員的內心會都“不寒而慄”。

構造表達式其實只有一條簡單的規則:按照使用的方式來聲明。

如何理解這句話呢?

float f, g;

這個聲明的含義是:當對其求值時,表達式f和g的類型爲浮點數類型。

float ff();

這個聲明的含義是:表達式ff()求值結果是一個浮點數,也就是說,ff是一個返回值爲浮點類型的函數。

float *pf;

這個聲明的含義是*pf是一個浮點數,也就是說,pf是一個指向浮點數的指針。

 

float *g(), (*h)()

聲明瞭這樣一個函數g:返回值爲指向浮點數的指針,參數列表爲空。

聲明瞭h是指向這樣一個函數---返回值float,參數列表爲空的指針。

float fun()

{

}

h = fun;    //賦值

h();       //調用函數fun – 這是一種簡寫形式

(*h)();     //調用函數fun – 這是標準的形式

一但我們知道了如何聲明一個給定類型的變量,那麼該類型的類型轉換符就很容易得到了:只需要把聲明中的變量名和聲明末尾的分號去掉,在將剩餘的部分用一個括號整個“封裝”起來即可。例如,因爲下面的聲明:

float (*h)();

表示h是一個指向返回值爲浮點類型的函數的指針,因此,

(float (*)() )         //注意有個星號*

表示一個“指向返回值爲浮點類型的函數的指針”的類型轉換符。

上例中可以如下使用這個類型轉換符:

void *p = fun;

((float(*)())p)();  //調用fun

( * ( void ( * ) ( ) 0 ) ( );    ---- 硬件將調用首地址爲0位置的子例程

這個表達式就表示調用首地址爲0位置的子例程,子例程的原型是:返回值爲void的函數,其參數列表爲空。

有了typedef後這個問題就清晰多了

typedef void (*funcptr)();

(*(funcptr)0)();  //等價於 ( * ( void ( * ) ( ) 0 ) ( ); 

8 運算符的優先級

拿不準的情況下最好用括號。 

9 指針與數組

C語言中只有一維數組,而且數組的大小必須在編譯期就作爲一個常數確定下來。然而數組元素可以是任意類型(所以可以定義多維數組)。

對於一個數組,我們只能做兩件事:確定該數組的大小,以及獲得指向該數組下標爲0的元素的指針。以數組下標進行的運算實際上是通過指針進行的。

int calendar[12][31];

如果calendar不是用於sizeof的操作數,二是用於其他的場合,那麼calendar總是被轉換成一個指向calendar數組的起始元素的指針。 

指針的類型很重要,加一減一操作是根據其類型定義的。void類型的指針就不能進行加一減一操作,因爲不知道它所指向的元素的大小。 

int a[5][3] = { 1, 2, 3,

                     4, 5, 6,

                     6, 7, 8,

                     9, 10, 11,

                     12, 13, 14};

a 是指向有五個元素的一維數組的地址,其中每個元素又包含三個int型整數。

printf(“%d\n”, **(a+3)) 打印輸出9

sizeof(a[1]) = 12

sizeof(*(a+1)) = 12;

*(a+1) 指向數組第二個元素的地址,而這個元素是一個包含3個int型整數的一維數組。 

int (*ap)[31];  //指向數組的指針

int *p[10];    //指針數組 

10 作爲參數的數組聲明

int strlen (char s[]) 與 int strlen (char *s) 等價

需要注意的是如果一個指針參數並不實際代表一個數組,即使從技術上而言是正確的,採用數組形式的記法經常會起到誤導作用。

main (int argc, char *argv[]) 與 main (int argc, char **argv) 等價,前一種寫法強調的重點在於argv是一個指向某數組的起始元素的指針,該數組的元素爲字符指針類型。因爲這兩種寫法是等價的,所以我們可以任選一種最能清楚反映自己意圖的寫法。 

11 邊界計算與不對稱邊界

int i, a[10];

for (i=0; i<=10; i++)

       a[i] = 0;

如果用來編譯這段程序的編譯器按照內存地址遞減的方式來給變量分配內存,那麼這段程序將陷入死循環。因爲a[10] = 0 就相當於i=0,這樣當i遞增到10時又回到0,循環就這樣繼續下去。 

有的語言數組下標是從1開始的,C語言的數組下標是從0開始的。這種數組的上界(即第一個“出界點”)恰是數組元素的個數!

避免“欄杆錯誤”(涉及邊界計算時常出的錯誤)的兩個通用原則:

1 特例外推       考慮最簡單情況下的實例,然後將結果外推。

2 仔細計算邊界

編寫這樣一個函數:函數buffwrite有兩個參數,第一個參數是一個指針,指向將要寫入緩衝區的第一個字符;第二個參數就是一個整數,代表將要寫入緩衝器的字符數。假定我們可以調用函數flushbuffer來把緩衝區中的內容寫出,而且函數flushbuffer會重置指針bufptr,使其指向緩衝區的起始位置。

在下面的兩個例子中,我們要小心“欄杆錯誤”。

version 1

void bufwrite (char *p, int n)

{

       while (--n >= 0)

       {

              if (bufptr == &buffer[N])

                     flushbuffer();

              *bufptr++ = *p++;

       }

}

優化的version 2 

12 main返回值

典型的處理方案是,返回值爲0代表程序執行成功,返回值非0則表示程序執行失敗。如果一個程序的main函數並不返回任何值,那麼有可能看上去執行失敗。所以穩健的做法就是始終顯示返回正確的值。 

13 連接器

連接器:不理解C語言,然而它能夠理解機器語言和內存佈局。

典型的連接器把由編譯器或彙編器生成的若干目標模塊,整合成一個被稱爲可載入模塊或可執行文件的實體,該實體能被操作系統直接執行。

 

編譯器:把C源程序“翻譯”成連接器能夠理解的形式。

簡單的編譯流程圖: 

 

14 聲明與定義

有時候聲明也是定義

一個聲明就是一個定義,除非聲明:引入名稱 定義:引入實體。

extern int a

extern 顯示地說明了a的存儲空間是在程序的其它地方分配的。

每個外部對象都必須在某個地方進行定義。

static int a

static修飾符是一個能夠減少命名衝突的有用工具。上面的定義中,a的作用域限制在單個文件中。

保證同一個對象只有一種類型(定義與聲明統一)、並保證只在一個地方定義。 

15 文件訪問

C中要同時對打開的文件進行輸入和輸出操作,必須在其中插入fseek函數的調用。 

16 緩衝輸出與內存分配

設置輸出緩衝區

setbuf(stdout, buf)

調用fflush刷新緩衝區

設置緩衝區後,數據會先緩衝在緩衝區知道緩衝區滿才輸出。

緩衝區的大小由系統頭文件<stdio.h>中的BUFSIZ定義。 

17 使用errno檢測錯誤

應在確認出錯後,再檢查 errno。而不應該直接檢查errno判斷是否出錯。 

18 signal函數

讓signal處理函數儘可能的簡單。比如不要在signal處理函數中使用malloc。 

一、詞法陷阱:

1)=不同於==;
2)&和|不同於&&和||;
3)詞法分析中的“貪心法”,例如a---b 會被解析成a-- -b而不是a- --b;
4)整形常量陷阱:046不同於46,原因是046會被解析成八進制,而不是十進制;
二、語法陷阱:
1)運算符的優先級問題,解決辦法:加括號;
2)語句結束時的分號,例如:if(x[i] > big); big=x[i];;
3)switch語句的break問題; 
4)else匹配問題,else始終與同一對括號內最近的未匹配的if結合;
三、語義陷阱:
1)指針與數組有時候不是完全等價的;
2)能使用數組的邊界指針來進行比較等操作,但不能涉及其具體內容的調用。
   例如:char num[5]; char * p; 可以p=&num[5];但不可以strcpy(p,&num[5]);
3)指針複製問題,複製指針並不會同時複製指針所指向的數據,而只會讓兩個指針指向內存中的同一地址;
4)欄杆錯誤:修一個100米的護欄,護欄的欄杆之間相距10米,問需要多少根欄杆?答案是11根,而不是10根;
5)整數溢出問題,比如說兩個正整數相加,結果卻是負數,或者一個負數取絕對值的時候也有可能會出問題,因爲在有符號整型變量中負數的表示範圍比正數大;
6)爲main函數提供返回值,如果沒有說明main的返回類型並且沒有返回值的時候,默認會返回一個整型隨機數,這有可能會讓系統覺得該函數運行失敗;
四、預處理問題:
1)宏定義中的空格問題,#define f (x) (x+1)的含義是f代表(x) (x+1)而不是f(x)代表(x+1)
2)宏不是函數,宏定義中所有的變量都要用括號括起來,防止引起與優先級有關的問題。
   例如:#define abs(x) x>0?x:-x
abs(a-b)會被展開成a-b>0?a-b:-a-b
3)宏的參數中如果使用自增或自減函數有可能會出問題
   例如:#define max(a,b) ((a)>(b)?(a):(b))
int a=6; max(a++,5);會被展開成a++>5?a++:5;
4)宏不是語句,如果在宏中使用到了if可能會產生陷阱
   例如:#define assert(e) if(!e) assert_error(_FILE_,_LINE_)
if(x>0 && y>0)
assert(x>y);
else
assert(y>x);
5)宏定義指針型變量的時候,有可能會出現陷阱
   例如: #define int_ptr int*
int_ptr a,b;//這時候,a的類型是int*,而b的類型卻是int,所以此時應該用typedef來解決變量重命名問題
五、更新書序文件
爲了保持與過去不能同時進行讀寫操作的程序的向下兼容性,一個輸入操作不能隨後緊跟一個輸出操作,反之亦然。即fread和fwrite之間必須要插入fseek函數的調用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章