《C陷阱和缺陷》讀書筆記(1)

                                       Chapter 1 詞法陷阱

    程序中的單個字符孤立起來看並沒有什麼意義,只有結合上下文才有意義,如p->s = "->";兩處的-意義
是不同的。

    程序的基本單元是token ,相當於自然語言中的單詞。 一個token的意義是不會變的。 而組成token 的字
符序列則隨上下文的不同而改變。

    token之間的空格將被忽略。


1.1 = 不同於 ==

1.2 &和|不同於&&和||

1.3 詞法分析中的貪心法

    token分爲單字符token和多字符token,如/ 和 == ,當有岐義時,c語言的規則是:每一個token應包括
    儘可能多的字符。

    另外token的中間不能有空白(空格,製表符, 換行符)
    y = x /*p 應寫爲y = x / *p  或者y = x / (*p);

    老編譯器允許用=+來代表現在+=的含義。所以它們會將a=-1理解爲a=- 1 即a = (a-1);
它們還會將複合賦值語句看成兩個token,於是可以處理 a>> =1, 而現代的編譯器會報錯。


1.4 整型常量

    常量前加0代表是8進制。

1.5 字符與字符串

    用雙引號引起的字符串, 代表的是一個指向無名數組起始字符的指針


    a+++++b的含義是什麼?

    C不允許嵌套註釋。


                                      Chapter 2  語法陷阱


2.1 構造函數聲明

    構造函數聲明的規則:按照使用的方式來聲明。

    任何C聲明都由兩部分組成:類型及類似表達式的聲明符(declarator)。


    float *g(), (*h)();
    g是一個函數,該函數的返回值類型爲指向浮點數的指針。 h是一個函數指針, h所指向函數的返回值爲
浮點類型。()的優先級高於*。

    因爲float (*g)();表示g是一個指向返回值爲浮點類型的函數的指針。所以(float (*)())表示一個“指向
    返回值爲浮點類型的函數的指針”的類型轉換符。


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

    (*(void(*)())0)()表示什麼意思呢?

    如果fp是一個函數指針, 那麼(*fp)()就表示對其所指的函數的調用。簡寫爲fp()。但這只是簡寫而已。
    而*((*fp)())可以簡寫爲*fp()

    根據上文(void(*)()) 表示一個“指向返回值爲void的函數的指針”的類型。這裏不過是對0作強制轉換而
已。其實用typedef更好:

    typedef void (*funcptr)();
    (*(funcptr)0)();

    signal的聲明如下:
    void (*signal(int, void(*)(int)))(int);
    或者用typedef:
    typedef void (*HANDLER)(int);
    HANDLER signal(int, HANDLER);


2.2 運算符的優先級問題

    注意條件運算符優先級比賦值運算符高,書上第22頁是錯的。
    & > ^ > |

2.3 分號

2.4 switch 語句

2.5 函數調用

    f();
    是個函數調用。而f;則計算函數f的地址。
2.6 else

    C語言允許初始化列表中出現多餘的逗號。


       
                                       Chapter 3 語義陷阱


3.1 指針與數組

    C語言中只有一維數組, 而且數組的大小必須在編譯期間就作爲一個常數確定下來。多維數組是通過一維
數組仿真的,因爲數組的元素可以是任何對象,當然也可以是數組。

    對數組,我們只能做兩件事,確定其大小,以及獲得指向該數組下標爲0的元素的指針。其它的有關數組
的操作,實際上是通過指針進行的。


    如果兩個指針指向的是同一個數組中的元素,我們可以把這兩個指針相減。如果它們指向的不是同一個數
組中的元素,即使它們指向的地址在內存中的位置正好間隔一個數組元素的整數倍,所得的結果仍然是無
法保證其正確性的。

    如果在應該出現指針的地方出現了數組名,則數組名就被當作指向該數組下標爲0的元素的指針。
     int a;
     p = a;
     int *p;
是對的。但p = &a在ansi C中則是非法的。因爲&a 是一個指向數組的指針,而p是一個指向整型變量的指針,
它們的類型不匹配。
    由於a[i] 即*(a+i);而a+i即i+a;所以a[i]即i[a];但不推薦後者的寫法

    int cal[12][31];
    int *p;
    int i;

    i = cal[4][7]等於i = *(cal[4] + 7);也等於i = *(*(cal + 4) +7);


    p = cal; 是錯誤的,類型不匹配,後者是指向數組的指針。


    我們來聲明指向數組的指針:
    int (*ap)[31];
    於是我們可以這樣寫:
    int cal[12][31];
    int (*monthp)[31];
    monthp = cal;

    兩 個指針不能相加。負數的移位運算不等於相應的乘或除運算。

3.2 非數組的指針

    我們要將s和t連接成r.
    s = "abc";
    t = "efg";
    char *r;
    strcpy(r,s);
    strcat(r,t);
這並不能達到目的。

因爲一是不能確定r指向何處, 二是不能保證r所指向的地址處還應該有內存空間可供容納字符串。

較好的是把第一行改爲char r[100];只是這樣的話,大小固定了。

正確的應該是:

#include <stdio.h>
#include <ctype.h>

int main (void)
{
     char s[10];
     char t[10];
     
     char *r;
     char *malloc();
     r = malloc(strlen(s) + strlen(t) + 1);
     if(!r)
     {
          complain();
          exit(1);
     }
     
     scanf("%s",s);
     /*getchar();*/
     
     scanf("%s",t);
     
     strcpy(r,s);
     strcat(r,t);
     
     printf("%s\n",r);
     free(r);
     
}

3.3 作爲參數的數組聲明

    我們沒有辦法將一個數組作爲函數參數直接傳遞。數組名會被轉爲指向該數組第一個元素的指針。

    int strlen(char s[]){}
    與下面的寫法完全相同:
    int strlen(char* s){}

    但其它地方就未必相同了。
    下面兩 個語句是完全不同的。
    extern char *hello;
    extern char hello[];

    下面則是一樣的
    main(int argc, char* argv[]){}
    main(int argc, char** argv){}

3.4 避免“舉隅法”

    複製指針並不同時複製指針所指向的數據。

3.5 空指針並非空字符串

    把常數0轉爲指針,則指針不等於任何有效的指針,即 void 指針。其它將整數轉爲指針得到的結果未定
義。當常數0被轉爲指針時,這個指針絕對不能被解除引用(dereferenc)。換句話說,當我們將0賦給一個指
針變量時,絕對不能企圖使用該指針所指向的內存中存儲的內容。

    下面的是合法的:
    if (p == (char *) 0)
    但下面是非法的
    if (strcmp(p, (char *) 0) == 0)

    如果p是一個空指針,即使printf(p);和printf("%s",p);的行爲也是未定義的。

3.6 邊界計算與不對稱邊界

    數組的下標如果用入界口加出界口來表達(即10個元素,其下標爲0 <= n < 10 ),則元素個數即爲上界與下界
之差,即下界。若爲空,則上界等於下界。任何情況下上界也永遠不可能小於下界。

    儘量採用非對稱邊界法。
    一個有N個元素的數組 ,我們可以使用a[N]進行比較和賦值,但不能引用其內容。


3.7 求值順序

    C語言只有四個運算符(&&, ||, ?: , 和 ,)存在規定的求值順序。另外,分隔函數參數的逗號並非逗號
    運算符。例如,在x和y在函數f(x,y)中的求值順序是未定義的,而在函數g((x,y))是先算x,再算y,y
    的值爲參數。特別是賦值運算符沒有規定求值順序。


3.9 整數溢出

    無符號算術運算中,沒有所謂的“溢出”一說。有符號運算中發生溢出,則結果未定義。

    下面檢測溢出的方法不可靠:
    if(a + b <0)
    complain();

    應該這樣:
    if((unsigned) a + (unsigned) b >INT_MAX)
    complain();

或者這樣
        if(a > INT_MAX - b)
        complain();

3.10 爲函數main提供返回值

     如果沒 有爲函數聲明返回類型,則默認爲int.



free之後最好馬上就p = NULL;


                                         Chapter 4 連接



4.1 什麼是連接器
    連接器通常把目標模塊看成是由一組外部對象組成的。 第個外部對象都代表着機器內存中的某個部分,並
通達一個外部名稱來識別。因此, 程序中的每個函數和每個外部變量,如果沒有被聲明爲static,就都是一個
外部對象。 某些編譯器會對靜態函數和靜態變量的名稱做一定改變,將它們也作爲外部對象。

    除了外部對象,目標模塊還可能包括了對其它模塊中的外部對象的引用。


4.2 聲明與定義


    每個外部變量只能定義一次。

4.3 命名衝突與static修飾符

4.4 形參、實參與返回值

    每個函數都要在調用之前進行聲明定義,不然返回類型爲int
    如果一個函數沒有float,short或者char類型的參數,在函數聲明中完全可以省略類型聲明(定義不能省
    略)

4.5 檢查外部類型

    同一個外部變量在不同的地方被聲明爲不同的類型,這種錯誤大部分編譯器是檢不出來的。
    char file[]= "/etc/password";
    與
    extern char* file;
是不一樣的。

4.6 頭文件

                                        Chapter 5 庫函數


    C標準沒有定義執行底層I/O操作的read和write函數。
5.1 返回整數的getchar函數

5.2 更新順序文件

    爲了與以前的程序保持兼容,一個輸入操作不能隨後緊跟一個輸出操作,反之亦然。如果要同時進行輸入
    和輸出操作,必須在其中插入fseek函數的調用。

    FILE *fp;
    struct record rec;
   
    while (fread((char *)&rec, sizeof(rec),1,fp) = 1)
    {
        /*    */
        if(/* */)
        {
          fseek(fp, -(long)sizeof(rec), 1);
          fwrite((char *)&rec, sizeof(rec), 1,fp);
          fseek(fp, 0l,1);
         }
}

5.3 緩衝輸出與內存分配

    #include <stdio.h>

void main(void)
{
     int c;
     char buf[BUFSIZ];
     setbuf(stdout,buf);
     
     while((c = getchar()) != EOF)
          putchar(c);
}
這個是不對的。buf最後一次被清空是在什麼時候?答案是在main函數結束之後,作爲程序交回控制給操作系
統之前C運行時庫所必須進行的清理工作的一部分。但是在此之前buf已經被釋放。

    解決方法一是加上static 聲明。也可以把buf聲明完全移到main函數之外。第二種辦法是動態分配緩衝區,
在程序中並不主動釋放分配的緩衝區


5.4 使用erron檢測錯誤

    很多的庫函數,特別是那些與操作系統有關的,當執行失敗時會通過一個名稱爲errno的外部變量,通知
程序該函數調用失敗。

    下面的是錯誤的:
    /*調用庫函數*/
    if(errno)
        /*處理錯誤*/
   
    因爲,在庫函數調用沒有失敗的情況下,並沒有強制要求庫函數一定要設置errno爲0,這樣errno的值可能
    就是前一個執行失敗的庫函數設置的值。
    下面更正了,可還是錯誤的:
    errno = 0;   
    /*調用庫函數*/
    if(errno)
     /*處理錯誤*/

     庫函數在調用成功時,既沒有強制要求對errno清零,但同時也沒有禁止設置errno。

     下面纔是對的:

     /* 調用庫函數 */
     if(返回的錯誤值)
        檢查errno

5.5 庫函數signal

    從理論上說,一個信號可能在C程序執行期間的任何時刻上發生,甚至可能出現在某些複雜的庫函數(如
malloc)的執行過程中。因此從安全的角度講,信號的處理函數不應該調用上述類型的庫函數。基於同樣的原
因,從signal處理函數中使用longjump退出,通常情況下也是不安全的:因爲信號可能發生在malloc 或者其它
庫函數開始更新某個數據結構,卻又沒有最後完成的過程中。因此signal處理函數能夠做的安全的事情,似乎
就只有設置一個標誌然後返回,期待以後主程序能夠檢查到這個標誌,發現一個信號已經發生。

    然而,就算這樣做也並不總是安全的。當一個算術運算錯誤引發一個信號時,某些機器在signal處理函
數返回後還將重新執行失敗的操作。因此對於算術運算錯誤,signal處理函數的惟一安全、可移植的操作就是
打印一條出錯消息,然後使用longjump或exit立即退出程序。
    當一個程序異常終止時,程序輸出的最後幾行常常會丟失,原因是緩衝。


                                       Chapter 6 預處理器

6.1 不能忽視空格

6.2 宏並不是函數

6.3 宏並不是語句

    #define assert(e) ((void)((e)||_assert_error(_FILE_,_LINE_)))
6.4 宏並不是類型定義

我們沒有辦法在一個C表達式的內部聲明一個臨時變量。
避免副作用的一個辦法就是再引入一個變量。

在某個上下文中本應需要函數而實際上卻用了函數指針,那麼該指針所指向的函數將會自動地被取得並替換這
個函數指針。


                                     Chapter 7 可移植性缺陷

7.1 應對C語言標準變更
7.2 標識符名稱的限制

    c標準所能保證的只是,c實現必須能夠區別出前6個字符不同的外部名稱,且並沒有要求區分大小寫。
7.3 整數的大小

    一個普通(int)整數足夠大以容納任何數組下標。

    字符長度由硬件決定
7.4 字符是有符號整數還是無符號整數

    若爲有符號,則將其轉爲int時,應該同時複製符號位,而無符號,則填 0即可。
    一個常見的錯誤是:如果c是一個字符變量,使用(unsigned)c就可得到與c等價的無符號整數。這是錯誤
    的,因爲在將字符c轉換爲無符號整數之前,c將先被轉爲int型,而此時可能得到非預期的結果。
    正確的是使用語句(unsigned char)c,這樣就直接轉換。
7.5 移位運算符

    如果被移位的對象長度是n位,那麼移位計數必須大於或等於0,而嚴格小於n。

    即使某些c實現將符號位複製 到空出的位中,有符號整數的向右移位運算也並不等於除以2的某次冪。
    (-1)>>1這一般不可能爲0,但(-1)/2一般爲0.

7.5 內存位置0

     NULL指針並不指向任何對象,只能用於賦值或比較運算。

7.7 除法運算的截斷

    q = a / b;
    r = a % b;
    C 語言的定義只保證q*b+r==a,以及當a>=0且b>0時,保證|r|<|b|以及r>=0.最好避免a爲負值。

7.8 隨機數的大小

    RAND_MAX

7.9 大小寫轉換

   
7.10 首先釋放,然後重新分配

     注意早期的C實現可以realloc一個已經free了的指示針。

7.11 一個例子

     因爲字符串常量可以用來表示一個字符數組,所以在數組名出現的地方都可以用字符串常量末端替換。
     如:
     "0123456789"[n%10]

     -n可能溢出,因爲最小負數的絕對值大於最大正數的絕對值。所以改亦正數的符號不會有問題,而改變
      負數的符號則可能有問題。

void printnum(long n, void (*p)())
{
        if(n<0)
        {
        (*) ('-');
        n=-n;
        }
        if(n>=10)
        printnum(n/10,p);
        (*p)((int)(n%10) + '0');
}
上面的是有問題的。下面的纔是對的:
void printneg(long n, void (*p)())
{
        long q;
        int r;
        q = n / 10;
        r = n % 10;
        if(r>0)
        {r -= 10;
        q++;
        }
if (n <= -10)
   printneg(q,p);
(*p)("0123456789"[-r]);
}

void printnum (long n, void (*p)())
{
        if(n < 0)
        {
        (*p)('-');
        printneg(n,p);
        }
        else
        printneg(-n,p);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章