本文通過兩個問題出發,跟蹤彙編代碼,闡明瞭函數 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語言中,浮點數有兩種數據類型float和double,float佔用32bit,double佔用64bit。float的存儲格式遵從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