先看下面幾個問題,如果你能准確地回答,那么此篇文章將不適合你:
- 計算機中怎樣表示浮點數的,與整型的表示方法有什么不同?
- 32位精度的float類型和64位精度的double類型能表示浮點數最大范圍是多少?
- 該C語言語句 printf("%d\n", 2.5); 輸出結果是什么,為什么?
我先說在此之前我如果回答,答案如下:
- 計算機中有符號整型采用補碼進行表示,浮點型怎么表示沒想過。
- float類型可以表示-232-1~232,double類型可以表示-264-1~264。
- 輸出格式要求輸出整型,而數是浮點型,類型轉化之后輸出結果為2。
有一點可以明確,我的回答都是錯誤的。那好吧,下面是我查看一些資料總結出來的,希望能解釋清楚其中的”奧秘“。
IEEE754標准(以下簡稱”標准“)是使用最廣泛的浮點數運算標准,為許多CPU與浮點運算器所采用。該標准定義了表示浮點數的格式,如下圖所示:
下面只討論二進制浮點數的表示,分成了三個部分:
符號位、指數、尾數,它們的含義可以類比科學計數法。如:
科學計數法中:
(102.35045)10 = +1.0235045 × 102 符號位為正,指數是2,尾數是1.0235045。
(-0.00023103)10 = -2.3103 × 10-4 符號位為負,指數是-4,尾數是2.3103。
同樣在規格化二進制浮點數中:
(1001.0111010)2 = +1.001011101 × 23 符號位為正,指數是3,尾數是1.001011101。
(-0.0001010011)2 = -1.010011 × 2-4 符號位為負,指數是-4,尾數是1.010011。
由上面的實例可以知道,在二進制浮點數被規格化后,尾數的格式都是1.****,指數表示將小數點移動多少位可以實現規格化(向左指數加1,向右指數減1),因此可以正也可為負。
標准同時規定:
- 符號位用1位表示,0表示正數,1表示負數;
- 指數采用移碼表示(原來的實際的指數值加上一個固定值得到的),這個固定值為2e-1-1(e為指數部分比特長度),之所以加上這個偏移量,是為了將負數變成非負數,這樣兩個指數的大小很容易就可以比較。
- 尾數采用原碼表示,正如上所說,規格化二進制浮點數最高位均為1,那么小數點前這個就沒必要用一個比特位去存儲,我們默認已經存在,稱為”隱藏位“。
標准規定了四種浮點數的表示方式:單精確度(32位)、雙精確度(64位)、延伸單精確度(43比特以上,很少使用)與延伸雙精確度(79比特以上,通常以80比特實做)。C語言中float和double浮點型分別對應的是單精度和雙精度浮點數,下面介紹這兩種浮點數的存儲格式:
如上面兩個例子,分別使用單精度和雙精度表示如下:
至此,應該已經解釋清楚了浮點數在計算機中的存儲格式和方法了,也就等於回答了上面的第一個問題,至於第二個問題,如果理解了上面所說的,求浮點數表示的范圍就應該很簡單了,下表為單精度浮點數各種極值情況:
至於最后一個問題,我們寫一個C語言程序進行測試:
#include <stdio.h> int main() { printf("%d\n", 2.5); return 0; }
編譯運行結果如下:
[guohl@guohl]$ gcc -o test test.c -g [guohl@guohl]$ ./test 0
運行結果和我們預期的2不一樣,使用gdb調試,在main函數處插入斷點,並且反匯編main函數之后得到:
(gdb) break main Breakpoint 1 at 0x8048415: file test.c, line 5. (gdb) run Starting program: /home/guohl/Documents/AS/test Breakpoint 1, main () at test.c:5 5 printf("%d\n", 2.5); (gdb) disassemble Dump of assembler code for function main: 0x0804840c <+0>: push %ebp 0x0804840d <+1>: mov %esp,%ebp 0x0804840f <+3>: and $0xfffffff0,%esp 0x08048412 <+6>: sub $0x10,%esp => 0x08048415 <+9>: fldl 0x80484e0 0x0804841b <+15>: fstpl 0x4(%esp) 0x0804841f <+19>: movl $0x80484d8,(%esp) 0x08048426 <+26>: call 0x80482f0 <printf@plt> 0x0804842b <+31>: mov $0x0,%eax 0x08048430 <+36>: leave 0x08048431 <+37>: ret End of assembler dump.
fldl addr 指令將內存addr中的雙精度浮點數加載到FPU寄存器堆棧,fstpl value 將雙精度數據從FPU寄存器堆棧出棧,保存到value中。因此,
0x08048415 <+9>: fldl 0x80484e0 0x0804841b <+15>: fstpl 0x4(%esp)
首先取出內存0x80484e0處的雙精度浮點數加載到FPU寄存器st0中,再從st0中取出放到esp-4處。先使用gdb -x命令查看內存0x80484e0處的內容:
(gdb) x/fg 0x80484e0 0x80484e0: 2.5 (gdb) x/2xw 0x80484e0 0x80484e0: 0x00000000 0x40040000 (gdb) x/8tb 0x80484e0 0x80484e0: 00000000 00000000 00000000 00000000 00000000 00000000 00000100 01000000
從上可以看到,以雙字的小數查看結果為2.5,由於我們平台采用的是小端格式存儲(little-edian,低位字節存儲在低內存位置),所以將以字節查看得到的結果恢復成下面的表示方法:
01000000 00000100 00000000 00000000 00000000 00000000 00000000 00000000
我們用IEEE754標准的雙精度格式解析上面這段二進制,符號位為0,即為正;指數位為10000000000(1024)減去偏移量1023為1;尾數0100…000,加上隱藏位1,為1.01(即十進制1.25)。所以結果為+1.25×21 = 2.5,符合我們的預期。
那么fstpl指令將該浮點數加載到esp-4處作為printf函數的參數,再接着指令“movl $0x80484d8,(%esp) ”將輸出格式控制符"%d" 的指針保存到esp指向的位置作為printf函數的函數,我們可以使用gdb查看內存0x80484d8處是不是格式控制符字符串:
(gdb) x/4cb 0x80484d8 0x80484d8: 37 '%' 100 'd' 10 '\n' 0 '\000'
確實如我們所想,現在在調用printf之前函數堆棧的結構如下所示:
進入printf函數,解析第一個參數輸出格式控制字符串,遇到%d,函數從之前壓棧的參數取出一個整型即取到上圖中esp+4處的值,以整型數輸出,為0。這就是我們上面運行./test 的輸出結果,而不是我想當然的程序會將2.5強制類型轉化為整型得到2!
參考資料:
http://zh.wikipedia.org/wiki/IEEE_754
Richard Blum, Professional Assembly Language