1. 程式人生 > >《C陷阱與缺陷》整理二

《C陷阱與缺陷》整理二

1.陣列名作實參
    在C語言中,我們沒有辦法將一個數組作為函式引數傳遞,如果我們使用陣列名作為引數,這個時候陣列名立刻會被轉換為指向該陣列的第一個元素的指標。
    關於這一點的理解可以向前深入一步,比如定義的陣列為int a[3],那麼a作為引數傳遞之後會變為int *型別;如果定義的陣列為int a[3][4],那麼a作為引數傳遞之後被變為int (*)[4];如果定義的陣列為int a[3][4][5],那麼a作為引數傳遞之後會變為int (*)[4][5];後續的以此類推。為什麼可以這樣呢?因為C語言中的多維陣列都是利用一維陣列模擬出來的,即一維陣列的每一個元素都可以是別的型別的資料單元,即便這個資料單元又是另一個數組,然而根據上面的觀點,一維陣列a在被作為引數傳遞的時候會自動退化為指向該一維陣列的第一個單元的指標,所以如果第一個單元是一個一維陣列,那麼a就退化為一個一維陣列指標,如果a的第一個單元是一個二維陣列,那麼a就退化為一個二維陣列指標,所以上面的結論是不難得出的。

2.看下面的程式碼片段輸出會是多少?

void print(int b[])
{
    printf("%d", sizeof(b));
}
int main(void)
{
    int a[4];
    print(a);
    return 0;
}
分析:要弄清楚這段程式碼片段的輸出,還是要清楚函式呼叫時候陣列的傳遞過程,上面第一點已經說過了,在傳遞引數的時候陣列已經自動被退化為指向其第一個單元的指標,所以在函式傳遞的過程中相當於出現了這樣的一個賦值的過程,int b[] = a或者更清楚一些int b[] = &a[0],但是這樣的語句編譯器會認為是一個錯誤的語法!但是實際中我們經常可能會這樣來使用卻並沒有報錯,這是因為編譯器在這裡會將b強制做一次退化,退化為一個int *的指標型別。所以上面的程式片段輸出內容顯而易見,輸出的就是一個int型別的指標變數的大小,也就是4(32位系統)。

3.main函式引數的兩種形式

int main(int argc, char *argv[])
int main(int argc, char **argv)
需要注意的是,前一種寫法強調的重點在於argv是一個指向某陣列的起始元素的指標,該陣列的元素為字元指標型別。

4.修改字串常量
以下的這種寫法:
    char *p = "xyz";
    p[0] = 'A';
編譯的器件可能不會產生問題,但是執行的時候很可能會碰到類似於某地址不能為written這種提示,K&RC中對這種修改行為的說明是:試圖修改字串常量的行為是未定義的。雖然有些編譯器允許這樣的行為,但是這種寫法是不值得提倡的。

5.空指標
    除了一個重要的例外情況,在C語言中將一個整數轉換為一個指標,最後得到的結果都取決於具體的C編譯器實現。這個特殊情況就是常數0,編譯器保證由0轉換而來的指標不等於任何有效的指標,出於程式碼文件化的考慮,常數0這個值經常用一個符號來代替:
#define NULL 0
需要記住的是當常數0被轉換為指標使用時,這個指標絕對不能被解除引用(解除引用即是使用(*p)這類取該地址中內容的操作),換句話說,當我們將0賦值為一個指標變數時,絕對不能企圖使用該指標所指向的記憶體中所儲存的內容。

6.C語言中“不對稱邊界”的好處

    在C語言中定義了一個數組int a[10]之後,陣列的下標0~9為合法的下標,而下標10已經超出了陣列的範圍。這樣做的好處是什麼呢?
第一個好處,請看下面的一個例子:
for(i = 0; i < 10; i++)
    a[i] = *p++;
如果使用者給出了begin(0)和end(10)的範圍之後要求對這之間的單元進行操作,如果使用者給定的begin和end是相同的話,上面這種寫法完全可以避免出現錯誤。同時要操作的單元個數可以通過end-begin簡單的就算出來,這樣做的前提就是使用者給出的begin和end都是遵守C語言的“不對稱邊界”使用方法。而如果不使用不對稱邊界時候(這時候陣列的下標為1~10合法)的諸如程式碼:
for(i = 1; i <= 10; i++)
    a[i] = *p++;
才可以完成對陣列的初始化或者遍歷等操作,這樣寫之後,實際操作的單元個數為10-1+1=10個,這樣的計算過程如果程式設計師在程式設計的時候忘了加上一個1那麼很容易造成程式的bug。同時如果將1和10換成begin和end變數的話,那麼使用者在呼叫這個函式的時候傳遞的begin和end值就算是同一個值,這段程式碼也會操作到陣列中的a[begin]值,這個也會造成呼叫者使用的困難。
第二個好處是我們可以將&a[10]來作為一個判斷條件,作為緩衝區或者陣列操作完成的一個標誌,這在實際程式設計中也是相當方便的。雖然對a[10]的值進行操作是屬於非法的行為,但是在ANSI中明確規定了&a[10]這種操作是合法的。

7.--n >= 0和n-- > 0
    在大多數的C語言實現中,--n >= 0至少與等效的n-- > 0一樣快,甚至在某些C實現中還要更快,第一個表示式--n >= 0的計算是首先從n中減去1,然後將結果與0比較;第二個表示式的計算則首先儲存n,然後從n中減去1,最後比較儲存值與0的大小。

8.求值順序
    C語言中只有四個運算子(&&、||、?:、,)存在規定的求值順序,運算子&&和運算子||首先對左側的運算元求值,只有在需要的時候才會對右側的運算元求值。運算子?:有三個運算元,在a?b:c中,運算元a首先被求值,根據a的值在求運算元b或者c的值(b和c只有一個表示式會被計算)。而逗號運算子,首先對左側的運算元求值,然後該值被“丟棄”,在對右側運算元求值,整個表示式的值是最右側表示式的值。
逗號運算子舉例:a = (1, 2, 3);
a最後被賦值為3。
注意:分隔函式引數的逗號並非逗號運算子,例如:f(x, y)中的求值順序是未定義的,而在函式g((x,y))中卻是確定的先x後y的順序,在後一個例子中,函式g只有一個引數,這個引數的值就是括號中逗號運算子的值。
注意:在C語言中其他所有運算子對其運算元求值的順序是未定義的。特別地,賦值運算子並不保證任何求值順序。如果在一個表示式中出現對同一變數的多次使用中出現了++或者--等操作後果有時是不可預計的。例如:
y[i] = x[i++];

9.邏輯運算的結果
    邏輯運算子的結果是一個邏輯值,即真(1)或假(0),而邏輯判斷的時候通常約定將0視作假,非0視作真。所以!10表示式的值為假(0),因為10非0在進行非運算的時候被視作真,真的非即為假。

10.溢位
    C語言中存在兩類整數算術運算,有符號數與無符號數運算。無符號數運算中沒有溢位的說法,然而有符號數操作就可能會發生溢位的情況,當一個運算的結果發生“溢位”時,作出任何假設都是不安全的。當碰到可能溢位的情況應該採取的方法是將兩個運算元a和b都強制轉換為無符號整數:
if((unsigned)a + (unsigned)b > INT_MAX)
    complain();
此處的INT_MAX是一個已定義常量,代表可能的最大整數值。ANSI C標準在<limits.h>中定義了INT_MAX;如果在其它的C語言實現上,讀者可能需要自己重新定義這個值。