深入解析printf/spintf/snprintf中的類型轉換

本文通過兩個問題出發,跟蹤彙編代碼,闡明瞭函數 printf/spintf/snprintf中的類型轉換的規律。

1 問題的提出

源碼1:

#include <stdio.h>
f1()
{
        double x = -5.5625;
        printf("%d\n",x);                //輸出爲0,爲什麼?
}
main()
{
        f1();
}
源碼2
#include <stdio.h>
f1()
{
        int y=1;
        printf("%f\n",y);          //輸出的值是隨機的, 爲什麼?
}
main()
{
        f1();
}
分析這個問題之前,我們要首先了解浮點數在計算機中是如何存儲的。

2 浮點數在計算機中的存儲

c語言中,浮點數有兩種數據類型floatdoublefloat佔用32bitdouble佔用64bitfloat的存儲格式遵從IEEE R32.24,參見圖1;而double的存儲格式遵從R64.53,參見圖2

 

1.1 float

float x= -5.5625;

接下來,詳細解析x在內存中的存儲格式。-5.5625 的二進制表示爲-101.1001,用二進制的科學計數法表示爲-1.011001*22

●符號位

由於是負數,所以爲1

●指數位

爲2。由於指數部分採用8bit存儲,取值範圍爲[-127,128],指數部分採用加127存儲,即在指數部分存儲的數據爲2+127=129,即10000001。

●尾數部分爲011001。所以x在內存中存儲格式見圖3。對應的16進制整數爲0xc0b20000。 

1.2 double

double y = -5.5625;

 -5.5625 的二進制表示爲-101.1001,用二進制的科學計數法表示爲-1.011001*22

 ●符號位

由於是負數,所以爲1

●指數位

爲2。由於指數部分採用11bit存儲,取值範圍爲[-1023,1024],所以指數部分採用加1023存儲,即在指數部分存儲的數據爲2+1023=1025,即100 0000 0001。

●尾數部分

爲011001。

 所以y在內存中存儲格式見圖4。對應16進制整數爲0xc01640000000 00 00。 

3 分析問題

以下是利用gdb跟蹤調試源碼1的過程。發現,printf("%d\n",x);根本就沒有把x由double類型轉換爲int類型,只是截取了x的低4個字節,並輸出。

(gdb) b main
(gdb) r
Breakpoint 1, main () at 1.c:9
9               f1();
(gdb) display /i $pc
call   0x8048354 <f1>
(gdb) si
push   %ebp                  ;保存上層函數的棧的上下文
(gdb) si
mov    %esp,%ebp       ;保存上層函數的棧的上下文
(gdb) si
sub    $0x28,%esp        ;爲函數f1分配的棧,大小爲28字節
(gdb) si
double x = -5.5625;
fldl   0x8048480             ;把0x8048480存儲的雙精度浮點數置入浮點寄存器%st(0)
(gdb) p/x (char[8])*0x8048480
$1 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x16, 0xc0}  ;證明了內存0x8048480處存儲常量-5.5625
(gdb) si
double x = -5.5625;
fstpl  0xfffffff8(%ebp)          ;把浮點寄存器%st(0)的值置入內存(%ebp-8)處
(gdb) info all-registers
st0            -5.5625  (raw 0xc001b200000000000000)  ;證明了%st(0)存儲的浮點數爲-5.5625
(gdb) si
printf("%x\n",x);
fldl   0xfffffff8(%ebp)    ;把內存(%ebp-8)處的雙精度浮點數置入%st(0),即-5.526
(gdb) 
printf("%x\n",x);
fstpl  0x4(%esp)                        ;把%st(0)中的值置入內存(%esp+4),即把printf的第二參數壓棧
(gdb) i r esp
esp            0xbfb00320       0xbfb00320
(gdb) p/x (char[8])*0xbfb00324
$2 = {0x6c, 0x95, 0x4, 0x8, 0x38, 0x3, 0xb0, 0xbf}
(gdb) si
printf("%x\n",x);
movl   $0x8048478,(%esp)   ;把函數printf的第一個參數壓入棧中,用棧來傳遞參數
(gdb) p/x (char[8])*0xbfb00324
;顯示printf的第二個參數的值。printf的格式串中”%d”在指明第二參數是int類型,即使實際傳遞的;是double類型,也沒有進行類型轉換,即沒有把x由double類型轉換爲int類型,printf在取值是
;直接讀取前4個字節00 00 00 00,所以printf輸出爲0
$3 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x16, 0xc0} ;  
(gdb) si
printf("%x\n",x);
call   0x8048298 <printf@plt> ;調用printf函數
(gdb) p/x (char[8])*0xbfb00324
$4 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x16, 0xc0}

由此引申開來發現如下規律:

● %d/%x/%u --> float/double

利用%d/%x/%u輸出float/double類型變量時,會得到意想不到的結果,因爲不會進行類型轉換,而是把變量截斷爲4個字節並輸出。原因在前面已經給出。

● %f --> int

利用%f輸出int變量,輸出的值是隨機的。

f1()
{
        int x = 1;
        printf("%f\n",x);  ;輸出的值是隨機的
}

對應的彙編代碼:

movl    $1, -4(%ebp)
movl    -4(%ebp), %eax
movl    %eax, 4(%esp)  ;沒有把x轉換爲float類型
movl    $.LC0, (%esp)
;printf會讀取內存4(%esp)除的8個字節,由於後4個字節的值是隨機的,所以輸出的值是隨機的
call    printf    

●%d/%x/%u  --> char/short

利用%d/%x/%u輸出char/short類型變量時,會對char/short類型進行符號位擴展,擴展爲4個字節。

f1()
{
         char x = 0x80;
        printf("%x\n",x);
} 

對應的彙編代碼:

movb    $1, -1(%ebp)
movsbl  -1(%ebp),%eax  ;把x符號擴展爲4個字節
movl    %eax, 4(%esp)
movl    $.LC0, (%esp)
call    printf


4 結論

由於float/double存儲機制迥異於char/short/int/long long的存儲機制,所以利用%f來輸出char/short/int/long long變量,或者是利用%d輸出float/double變量,得到的結果會令你大吃一驚。所以我們在使用printf/sprintf/snprintf函數時,應該嚴格的按照參數的類型指定格式串。

5參考

[1] http://www.cnblogs.com/jillzhang/archive/2007/06/24/793901.html

 

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