IEEE-754標(biāo)準(zhǔn)
目前世界上使用最為廣泛的小數(shù)表示方法是浮點(diǎn)數(shù)表示法,而浮點(diǎn)數(shù)通用的算術(shù)標(biāo)準(zhǔn)是IEEE-754標(biāo)準(zhǔn)溢十。什么是IEEE?
IEEE 電氣和電子工程師協(xié)會(IEEE,全稱是Institute of Electrical and Electronics Engineers)是一個美國的電子技術(shù)與信息科學(xué)工程師的協(xié)會拔鹰,是世界上最大的非營利性專業(yè)技術(shù)學(xué)會,其會員人數(shù)超過40萬人涣觉,遍布160多個國家痴荐。IEEE致力于電氣、電子官册、計(jì)算機(jī)工程和與科學(xué)有關(guān)的領(lǐng)域的開發(fā)和研究生兆,在航空航天、信息技術(shù)膝宁、電力及消費(fèi)性電子產(chǎn)品等領(lǐng)域鸦难,已制定了900多個行業(yè)標(biāo)準(zhǔn),現(xiàn)已發(fā)展成為具有較大影響力的國際學(xué)術(shù)組織
—— 引自百度百科
之所以要寫這么一篇文章员淫,是因?yàn)槲蚁胍愣?C語言對double
和float
的表示和存儲細(xì)節(jié)合蔽。之前自己弄懂了,不過由于沒有對文件進(jìn)行備份满粗,導(dǎo)致我的實(shí)驗(yàn)的代碼和筆記都被誤刪了辈末,連帶被誤刪的還有一篇探究字節(jié)序(大小端)的筆記和代碼,以及一篇關(guān)于開平方根算法和編碼的筆記(X86架構(gòu)有開平方根的及其指令映皆,超快)挤聘。這些使我意識到了將筆記轉(zhuǎn)化為文章并分享到網(wǎng)上的必要性。
這篇文章并非細(xì)致認(rèn)真的標(biāo)準(zhǔn)解讀手冊捅彻,只是想探究一下double
和float
的二進(jìn)制存儲序列组去。
C 中的浮點(diǎn)數(shù)表示
我們知道,C語言的浮點(diǎn)數(shù)分為單精度和雙精度步淹,單精度的float
采用位二進(jìn)制(
字節(jié))來存儲从隆,而雙精度的
double
使用位,另外還有一種占
位(
個字節(jié))的臨時數(shù)缭裆。
一個浮點(diǎn)數(shù)的存儲分為3個部分键闺,分別是符號位
,階碼
和尾數(shù)
澈驼,那么這三部分是如何組合而成為一個浮點(diǎn)數(shù)整體的呢辛燥?
對于一個位浮點(diǎn)數(shù),我們可以用下面的這張示意圖來表示它的各個部分的長度及順序缝其。其中一個等號
=
表示一個二進(jìn)制位挎塌,|
表示隱含的邊界。
|=|===========|===================================================|
|s|-exponent--|--------------------mantissa-----------------------|
上面的圖示中内边,s
(sign)為符號位榴都,占 bit,用來表示整個
double
的正負(fù)性漠其;中間部分exponent
是指數(shù)部分嘴高,即階碼竿音,占 bit;最后的也是最長的一部分
mantissa
拴驮,尾數(shù)谍失,占位,它的長度直接影響力浮點(diǎn)數(shù)的精度莹汤。
下面是這三個部分的具體細(xì)節(jié)
符號位跟整數(shù)一樣,符號位取
表示無符號颠印,為正數(shù)纲岭;取
表示有符號,為負(fù)數(shù)线罕。
指數(shù)部分采用的是移碼表示法并且采用的是余1023碼止潮,也就是說,指數(shù)部分的
位沒有符號位钞楼,在計(jì)算時喇闸,要先將這
位看作一個正整數(shù),然后減掉
之后才得真正的指數(shù)询件。
關(guān)于尾數(shù)部分燃乍,有一點(diǎn)需特別注意,尾數(shù)部分的值總是
1.M
宛琅,而1.M
中的1
是被隱藏了的。為什么呢?這樣做有什么意義眷篇?
我們知道池摧,一個十進(jìn)制數(shù)采用科學(xué)計(jì)數(shù)法表示的話,形式是其中
红伦。類推一下英古,就可以知道,一個
進(jìn)制的科學(xué)計(jì)數(shù)法昙读,小數(shù)的整數(shù)部分的取值范圍是
召调,所以在二進(jìn)制中,小數(shù)部分的取值范圍是
箕戳,這個范圍內(nèi)的實(shí)數(shù)某残,整數(shù)部分都是
,所以這個
1
是大家共有的陵吸,于是就沒有存儲的必要性了玻墅,因?yàn)?/p>
你愛,或者不愛
愛就在那里壮虫,不增不減
你存澳厢,或者不存
它就在那里环础,不大不小
上面講述了double
類型的存儲。一個double
占各二進(jìn)制位剩拢,而一個
float
則占用位线得,包括
位符號位、
位指數(shù)位和
位尾數(shù)位徐伐。
提取一個double
的各個部分
下面贯钩,我們用C語言編寫一個程序來打印一下一個double
的各個部分的二進(jìn)制及十進(jìn)制。相信理解了這段代碼办素,你就真的理解浮點(diǎn)數(shù)的表示了角雷。
#include <stdio.h>
#include <assert.h>
#define NM (1LL << 63) /* negative most */
#define PM ~NM /* positive most */
#define LL(d) *((long long*)&(d))
#define EZ(d) LL(d) &= (PM >> 1), LL(d) |= (1023LL << 52)
int sign(double d) { return (LL(d) >> 63) & 1LL; }
int exponent(double d) { return (LL(d) >> 52) & 0x7ff; }
// `EZ(d) -> LL(d) &= (PM >> 1), (LL(d) >> 52) & 0x7ff;`
// 將`d`的指數(shù)部分的$11$位填上$1023$(低$10$位全$1$,最高位為$0$)
// 因?yàn)椴捎玫氖怯?1023$碼性穿,所以這條語句的目的是將指數(shù)部分變?yōu)?0$
double mantissa(double d) { return EZ(d), d; }
#define sign(d) (sign(d)? -1 : 1)
#define exponent(d) (exponent(d) - 1023)
/* print binary of a double */
void printbd(double d)
{
/* from left to right */
printf("%+g =\n", d);
printf("%4c", 32);
for (int i = 0; i < 64; ++i) {
if (i == 0 || i == 1 || i == 12)
putchar('|');
// 這里只能用右移勺三,因?yàn)?(long long -> int) 要截?cái)嗟降?2位
putchar(((LL(d) >> (63 - i)) & 1LL) + '0');
}
printf("|\n");
printf("%4c", 32);
printf("%+d * %g * 2^(%d)\n", sign(d), mantissa(d), exponent(d));
}
int main()
{
assert(sign(+0.5) == +1);
assert(sign(-0.5) == -1);
printbd(-0.05);
printbd(+0.05);
printbd(-0.5);
printbd(+0.5);
printbd(-1.0);
printbd(+1.0);
printbd(-2.0);
printbd(+2.0);
printbd(-9.0);
printbd(+9.0);
printbd(-10.0);
printbd(+10.0);
return 0;
}
另外一些需要注意的細(xì)節(jié)
因?yàn)椴捎玫氖且拼a表示法,所以不像補(bǔ)碼表示法需曾,可以直接從二進(jìn)制判斷一個數(shù)的大小吗坚。指數(shù)部分全為時,指數(shù)部分的取值最大呆万。
對于正負(fù)無窮及不合法的運(yùn)算結(jié)果商源,IEEE標(biāo)準(zhǔn)規(guī)定
- 如果指數(shù)部分是
并且尾數(shù)的小數(shù)部分是
,則表示
(符號位相關(guān))桑嘶;
- 如果指數(shù)部分全為
炊汹,并且尾數(shù)的小數(shù)部分是
,則表示
逃顶;
- 如果指數(shù)部分全為
讨便,并且尾數(shù)的小數(shù)部分不為
,那么表示
Not a Number
以政,即霸褒。
附錄
前文代碼的運(yùn)行結(jié)果
-0.05 =
|1|01111111010|1001100110011001100110011001100110011001100110011010|
-1 * 1.6 * 2^(-5)
+0.05 =
|0|01111111010|1001100110011001100110011001100110011001100110011010|
+1 * 1.6 * 2^(-5)
-0.5 =
|1|01111111110|0000000000000000000000000000000000000000000000000000|
-1 * 1 * 2^(-1)
+0.5 =
|0|01111111110|0000000000000000000000000000000000000000000000000000|
+1 * 1 * 2^(-1)
-1 =
|1|01111111111|0000000000000000000000000000000000000000000000000000|
-1 * 1 * 2^(0)
+1 =
|0|01111111111|0000000000000000000000000000000000000000000000000000|
+1 * 1 * 2^(0)
-2 =
|1|10000000000|0000000000000000000000000000000000000000000000000000|
-1 * 1 * 2^(1)
+2 =
|0|10000000000|0000000000000000000000000000000000000000000000000000|
+1 * 1 * 2^(1)
-9 =
|1|10000000010|0010000000000000000000000000000000000000000000000000|
-1 * 1.125 * 2^(3)
+9 =
|0|10000000010|0010000000000000000000000000000000000000000000000000|
+1 * 1.125 * 2^(3)
-10 =
|1|10000000010|0100000000000000000000000000000000000000000000000000|
-1 * 1.25 * 2^(3)
+10 =
|0|10000000010|0100000000000000000000000000000000000000000000000000|
+1 * 1.25 * 2^(3)