1. 程式人生 > >你可能不知道的printf

你可能不知道的printf


前言

printf可能是我們在學習C語言的過程中最早接觸的庫函數了。其基本使用想必我們都已經非常清楚了。但是下面的這些情況你是否已經清楚地知道了呢?

示例程式

我們來看一個示例程式,看看你能否對下面的結果輸出有非常清晰的認識。

#include <stdio.h>
int main(void)
{
    int a = 4;
    int b = 3;
    int c = a/b;
    float d =  *(float*)(&c);
    long long e = 0xffffffffffffffff;
    printf("a/b:%f,a:%d\n",a/b,a,b);          //列印0

    printf("(float)a/b:%f\n",((float)a)/b);   //列印1

    printf("(double)a/b:%lf\n",((double)a)/b);//列印2

    printf("d:%f\n",d);                       //列印3

    printf("%.*f\n",20,(double)a/b);          //列印4

    printf("e:%d,a:%d\n",e,a);                //列印5

    printf("a:%d,++a:%d,a++:%d\n",a,++a,a++); //列印6

    return 0;
}

編譯為32位程式:

gcc -m32 -o test test.c

在執行之前,你可以自己先猜想一下列印結果會是什麼。實際執行結果:

a/b:0.000000,a:3        //列印0的結果
(float)a/b:1.333333      //列印1的結果
(double)a/b:1.333333     //列印2的結果
d:0.000000               //列印3的結果
1.33333333333333325932   //列印4的結果
e:-1,a:-1                //列印5的結果
a:6,++a:6,a++:4          //列印6的結果

你的猜想是否都正確呢?如果猜想錯誤,那麼接下來的內容你就不應該錯過了。

你是否會有以下疑問:

  • 0.列印0的a/b為什麼不是1,a為什麼不是4?

  • 1.列印1和列印2有什麼區別呢?

  • 2.列印3為什麼結果會是0.000000?

  • 3.列印4的結果為什麼最後的小數位不對?其中的*是什麼意思?

  • 4.列印5中,為什麼a的值是-1而不是4?

  • 5.列印6中,結果為什麼分別是6,6,4?

在解答這些問題之前,我們需要先了解一些基本內容。

可變引數中的型別提升

printf是接受變長引數的函式,傳入printf中的引數個數可以不定。而我們在變長引數探究中說到:
呼叫者會對每個引數執行“預設實際引數提升",提升規則如下:
——float將提升到double
——char、short和相應的signed、unsigned型別將提升到int

也就是說printf實際上只會接受到double,int,long int等型別的引數。而從來不會實際接受到float,char,short等型別引數
我們可以通過一個示例程式來檢驗:

//bad code
#include<stdio.h>
int main(void)
{
    char *p = NULL;
    printf("%d,%f,%c\n",p,p,p);
    return 0;
}

編譯報錯如下:

printf.c: In function ‘main’:
printf.c:5:12: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘char *’ [-Wformat=]
     printf("%d,%f,%c\n",p,p,p);
            ^
printf.c:5:12: warning: format ‘%f’ expects argument of type ‘double’, but argument 3 has type ‘char *’ [-Wformat=]
printf.c:5:12: warning: format ‘%c’ expects argument of type ‘int’, but argument 4 has type ‘char *’ [-Wformat=]

我們可以從報錯資訊中看到:

  • %d 期望的是 int 型別引數

  • %f 期望的是 double 型別引數

  • %c 期望的也是 int 型別引數

而編譯之所以有警告是因為,char *型別無法通過預設實際引數提升,將其提升為int或double。

引數入棧順序以及計算順序

在C語言中,引數入棧順序是確定的,從右往左。而引數的計算順序卻是沒有規定的。也就是說,編譯器可以實現從右往左計算,也可以實現從左往右計算。

浮點數的有效位

對於double型別,其有效位為15~~16位(參考:對浮點數的一些理解)。

可變域寬和精度

printf中,*的使用可實現可變域寬和精度,使用時只需要用*替換域寬修飾符和精度修飾符即可。在這樣的情況下,printf會從引數列表中取用實際值作為域寬或者精度。示例程式如下:

#include<stdio.h>
int main(void)
{
    float a = 1.33333333;
    char *p = "hello";
    printf("%.*f\n",6,a);
    printf("%*s\n",8,p);
    return 0;
}

執行結果:

1.333333
   hello

而這裡的6或者8完全可以是一個巨集定義或者變數,從而做到了動態地格式控制。

格式控制符是如何處理引數的

printf有很多格式控制符,例如%d,它在處理輸入時,會從堆疊中取其對應大小,即4個位元組作為對應的引數值。也就是說,當你傳入引數和格式控制符匹配或者在經過型別提升後和格式控制符匹配的時候,引數處理是沒有任何問題的。但是不匹配時,可能會出現未定義行為(有兩種情況例外,我們後面再說)。例如,%f期望一個double(8位元組)型別,但是傳入的引數是int(4位元組),那麼在處理這個int引數值,可能會多處理4個位元組,並且也會造成處理資料錯誤。

真相大白

有了前面這些內容的鋪墊,我們再來解答開始的疑問:

  • 對於問題0,a/b的結果顯然為4位元組的int型別1,而%f期望的是8位元組的double,而計算結果只有4個位元組,因此會繼續格式化後面4個位元組的a,而整型1和後面a組合成的8位元組資料,按照浮點數的方式解釋時,它的值就是0.000000了。由於前面已經讀取解釋了a的內容,因此第二個%d只能繼續讀取4個位元組,也就是b的值3,最終就會出現列印a的值是3,而不是4。

  • 對於問題1,實際上在printf中,是不需要%lf的,%f期望的就是double型別,在編譯最開始的示例程式其實就可以發現這個事實。當然了在scanf函式中,這兩者是有區別的。

  • 對於問題2,也很簡單,2的二進位制儲存形式按照浮點數方式解釋讀取時,就是該值。

  • 對於問題3,double的有效位為15~16位,也就是之外的位數都是不可靠的。printf中的*可用於實現可變域寬和精度,前面已經解釋過了。

  • 對於問題4,這裡不給出,留給讀者思考,歡迎大家可留言區給出原因。

  • 對於問題5,雖然引數計算順序沒有規定,但是實際上至少對於gcc來說,它是從右往左計算的。也就是說,先計算a++,而a++是先用在加,即壓入a=4,其後,a的值變為5;再計算++a,先加再用,即壓入a=5+1=6;最後a=6,壓入棧。最終從左往右壓入棧的值就分別為6,6,4。也就是最終的列印結果。但是實際情況中,這樣的程式碼絕對不該出現!

至此,真相大白。

總結

雖然我們前面解釋了那些難以理解的現象,同時讀者可以參考變長引數探究對浮點數的一些理解找到更多的資訊。但是我們在實際程式設計中應該注意以下幾點:

  • 格式控制符應該與對應引數型別匹配或者與型別提升後的引數型別匹配。

  • 絕對避免出現計算結果與引數計算順序有關的程式碼。

  • *在printf中實現可變域寬和精度。

  • printf不會實際接受到char,short和float型別引數。

  • 如果%s對應的引數可能為NULL或者對應整型,那將是一場災難。

  • 不要忽略編譯器的任何警告,除非你很清楚你在做什麼。

  • 例外情況指的是有符號整型和無符號整型之間,以及void*和char*之間。

問題思考

如果編譯為64位程式執行,結果還是一樣嗎?為什麼?