關於C語言函式的原型宣告與函式定義,這裡涉及了許多的內容,以此文章,記錄自己所瞭解的知識,以備日後查閱,同時也幫助自己和大家瞭解這當中暗含的“陷阱”。

由於歷史的原因,C語言的函式宣告有舊式和新式之分,舊式就是K&R,而新式則是ANSI,如下圖:
這裡寫圖片描述

現在倡導的是使用後者,而不要使用前者,對於K&R,由於存在大量舊式程式碼,為了保持相容,所以沒有被正式廢棄。
這兩者在引數傳遞的時會有所區別,在K&R中,由於函式的引數也是表示式,所以會發生型別提升,即傳遞一個短於int的整數,函式實際所接收到的是int,如果傳遞的是float,函式實際接收到的是double,在被呼叫函式的函式體內,這些值會根據函式定義時引數的宣告型別自動裁減為該型別。
你可能會感到困惑,為什麼不嫌麻煩將它們提升為更大的型別,然後又直接把它們裁減為原來的大小?之所以這樣做,原意是為了簡化編譯器—所有的東西都是同一長度。如果只固定使用幾種型別,將大大簡化引數的傳遞。所有的引數都統一為標準長度,被呼叫函式會根據需要對它們進行裁剪。
相反在ANSI中,如果使用了適當的函式原型,型別提升便不會發生,如果引數宣告為char,則實際傳遞的也是char。使用新風格的函式定義,編譯器就會假定引數是準確宣告的,於是便不進行型別提升,並據此產生程式碼。

關於宣告和定義,我們需要考慮4種情況

  1. K&R C函式宣告和K&R C函式定義
    能夠順利呼叫,所傳遞的引數會進行型別提升
  2. ANSI C函式宣告(原型)和ANSI C函式定義
    能夠順利呼叫,所傳遞的引數位實際引數
  3. ANSI C函式宣告(原型)和K&R C函式定義
    如果使用一個較窄的型別就會失敗,函式呼叫時所傳遞的是實際型別,而函式期望接收的是提升後的型別
  4. K&R C函式宣告和ANSI C函式定義
    如果使用一個較窄的型別就會失敗,函式呼叫時所傳遞的是提升後的型別,而函式期望接收的是實際型別

所以,如果為一個K&R C函式定義增加函式原型,而原型的引數列表中有一個short引數,在引數傳遞時,這個原型將導致實際傳遞給函式的就是short型別的引數,而根據函式的定義,它期望接收的是一個int型別的引數。這樣,函式從堆疊中抓取4個位元組(int)而不是2個位元組(short)。

下面展示兩種失敗情況

/* 檔案1 */
/* 舊風格的定義,但它具有原型 */
int olddef (float d, char i);

int main(void)
{
    float d = 10.0;
    char j = 3;

    olddef (d, j);

    /* 新風格的定義,但它沒有原型 */
    newdef (d, j); 
}

----------------------------------------------------------------

/* 檔案2 */
/* 舊風格的定義,但它具有原型 */
olddef(d , i)
float d;
char i;
{
    printf ("olddef: float = %f, char = %x \n", d, i);
}

/* 新風格的定義,但它沒有原型 */
newdef(float d, char i)
{
    printf ("newdef: float = %f, char = %x \n", d, i);  
}

列印結果如下
這裡寫圖片描述

所以,堅決不要在函式的宣告和定義中混用新舊兩種風格。

還有一種錯誤的用法:

int main(void)
{
    union
    {
        double d;
        float f;
    }u;
    u.d = 10.0;
    printf ("put in a double, pull out a float f = %f \n", u.f);

    u.f = 10.0;
    printf ("put in a float, pull out a double d = %f \n", u.d);
}

列印的結果為:
這裡寫圖片描述