如同在十進位制中,1/3是無法被準確表示的,如果我們要將1/3轉換成十進位制小數的形式則是:
1/3 = 0.3333333....(3迴圈)
同理,十進位制小數0.1也是無法被二進位制小數準確表示,如果我們要將十進位制的0.1轉換為二進位制小數則是:
0.1 = 0.00011001100....(1100迴圈)
符號位:0(表示正數)
指數部分:01111011(01111011換算成十進位制是123,因為要減去-127故結果為-4)
尾數部分:10011001100110011001101(即通過移位之後,捨掉小數點左側的1,留下的小數部分,保留23位)
那麼這個用來「表示」十進位制小數0.1的float二進位制浮點數如果換算成十進位制數到底是多少呢?它和0.1到底有多大的誤差呢?下面我們就來換算一下:
指數部分:2^(-4) = 1/16
尾數部分:1 + 1/2 + 1/16 + 1/32 + 1/256 + 1/512 + 1/4096 + 1/8192 + 1/65536 + 1/131072 + 1/1048576 + 1/2097152 + 1/8388608 = 1.60000002384185791015625 (在換算成float時會把小數點左側的1省略,這裡需要再次加回來)
那麼,換算之後實際的十進位制數便是:1.60000002384185791015625 * 1/16 = 0.100000001490116119384765625
所以我們可以看到,二進位制浮點數並不能準確的表示0.1這個十進位制小數,它使用了0.100000001490116119384765625來代替0.1。
這便是直接使用二進位制來表示小數的方式,很有可能會產生誤差。
但是很多朋友都提到了使用decimal來避免上文**現的誤差。的確,使用decimal是乙個十分保險的措施。但是,為什麼使用decimal型別,計算機突然就能夠很完美的計算十進位制數了呢?難道是計算機在涉及到decimal型別的運算時,改變了自己內部最根本的二進位制運算嗎?
當然不是。
答案的確如此。但是在我們討論decimal的細節之前,我覺得有必要先簡單介紹一下decimal。
在這裡的decimal指的c#語言中的system.decimal,雖然在c#語言規範中只提到了兩種浮點數float和double(二進位制浮點數),但是如果我們了解浮點數的定義,decimal顯然也是浮點數——只不過它的底數是10,因此它是十進位制浮點數。
decimal的結構
同樣,decimal和float以及double的組成也十分類似:符號位、指數部分以及尾數部分。
當然,decimal有更多的位,總共達到了128位,換句話說它又16個位元組。如果我們把這16個位元組劃分成4個部分,就可以一窺它的組成結構了。
下面使用m表示尾數部分、e表示指數部分、s表示符號位:
1~4號位元組: mmmm mmmm mmmm mmmm mmmm mmmm mmmm mmmm
5~8號位元組: mmmm mmmm mmmm mmmm mmmm mmmm mmmm mmmm
9~12號位元組: mmmm mmmm mmmm mmmm mmmm mmmm mmmm mmmm
13~16號位元組: 0000 0000 0000 0000 000e eeee 0000 000s
從它的組成結構,我們可以看到decimal的尾數部分有96位(12位元組),而指數部分有效的只有5位,符號位自然只有1位。
decimal的尾數
現在讓我們把思路拉回本小節一開始的部分,如果通過借助整數來表示小數的方式,decimal便可以更準確的來表示乙個十進位制小數了。這裡我們就可以看到,decimal的尾數部分事實上是乙個整數,而尾數所表示的範圍也很明確了:0~2^96 - 1。換算為十進位制便是0~79228162514264337593543950335,乙個29位的數字(當然,最高位的值最多到7)。
此時如果我們對尾數部分進一步劃分結構的話,可以將尾數看成是由三個部分的整數組成的:
1~4號位元組(32位)代表了乙個整數,表示的尾數的低位部分。
5~8號位元組(32位)代表了乙個整數,表示的尾數的中間部分。
9~12號位元組(32位)代表了乙個整數,表示尾數的高位部分。
這樣,我們就將表示乙個整數的decimal尾數又劃分成了三個整數。
decimal的指數和符號
值得一提的還有指數部分,首先它也是乙個整數,但是如果我們進一步觀察decimal的結構的話,還可以發現指數部分的形式(000e eeee)很奇怪只有5位是有效的,這是因為它的最大值只能到28。至於為何要這樣處理,原因其實很簡單,decimal指數部分的底數是10,而尾數部分表示的是乙個29位或者28位的整數(之所以這樣說是由於最高位29的值其實只能到7,所以總共只有28位的值是可以任意設定的)。那麼就假設我們有乙個28位的十進位制整數,這28個位置上的值可以是0~9之中任何乙個數,此時decimal的指數部分控制的便是我們要在這個28位整數的哪一位點上小數點。
符號 * 尾數 / 10 ^指數
因此,decimal能正確表示的數字範圍位是-/+79228162514264337593543950335,但是也正是由於decimal可以表示的十進位制數字的有效位數也在28或29(取決於最高位的值是否在7以內)的範圍內,因此在表示小數的時候,對小數的位數也是有限制的。
decimal內部的4個整數
我們再回去看一眼decimal的結構,可以發現實際上128位中只有102位是必須的,除了這有意義的102位之外,其餘的位的值是0。而這102位我們可以進一步把它分成4個整數,這便是我們在呼叫decimal.getbits(value)方法時,返回的包含了4個元素的int型陣列:
其中前3個int型整數在上文我已經說過,它們用來表示尾數的低位部分中間部分以及高位部分。
最後的1個int型整數用來表示指數和符號部分。該int型整數中的0~15位並沒有使用,而是全部設為0;16~23位用來表示指數,當然由於指數最大值是28因此只有其中的5位有效;24~30位同樣沒有使用,而是全部設為0;最後一位存放的便是符號位,0代表正數,1代表負數。
下面我就來給各位舉乙個例子:
//我對這段**進行編譯並執行的結果如下圖:獲取decimal的組成結構
using
system;
using
system.collections.generic;
class
test
; console.writeline("
", "argument
", "
bits[3]
", "
bits[2]
", "
bits[1]",
"bits[0]");
console.writeline( "
", "--------
", "
-------
", "
-------
", "
-------",
"-------");
foreach(decimal val in
vals)
", val, bits[3], bits[2], bits[1], bits[0
]); }}}
通過上一段文字,我相信各位讀者應該已經發現了decimal其實並不神秘。也因此更加堅定了採用decimal來進行小數計算時一定會得到正確答案的信心。但是正如我在上文中所說的,decimal雖然提高了計算的準確度,但是它的有效位數也是有限的。尤其是在表示小數時,如果位數超過了它的有效位數,那麼可能會得到「錯誤」的答案。
比如下面的這個小例子:
//我們來編譯執行它:沒有注意有效位數而產生的錯誤
using
system;
class
test}}
可以發現7以內的結果都是正確的,而最後乘以8和乘以9的部分卻出現了錯誤。而產生這個結果的原因,其實我在上文中已經不止一次的提到過,那便是在29位有效數字情況下,最高位的值不能超過7才能獲得準確的值。而乘以8和乘以9顯然不符合這種要求。
1.迴避策略:即無視這些錯誤,根據程式目的的不同,有的時候一些誤差是可以接受的。這也是很好理解的,誤差在乙個可以允許的範圍內也是普遍存在於日常生活的中的。
2.把小數轉換成整數來計算:既然計算機使用二進位制進行小數計算時可能會有誤差,但是計算整數時一般是沒有問題的。因此,進行小數計算時可以暫時借助整數,只不過把最後的結果使用小數來表示便可以了。
蘇州沒有機場,又有神馬關係?
蘇州沒有機場,又有神馬關係?近期蘇州又有自 喧囂了,蘇州機場夢似乎有實現的希望了,說是 部門要繼續加強機場規劃云云,讓屢屢為蘇州機場夢沒能實現而悲憤不已的人為之興奮不已。筆者認為,蘇州人沒有必要把機場看的那麼重。無錫,常州,徐州有機場,也沒能怎麼著,也沒有創造奇蹟,更沒有上天 網紅城市成都武漢南京杭...
人月神話之沒有銀彈
讀 人月神話 也有了一段時間了,現在也理清了一些自己的思路了,這次主要是針對裡面的 沒有銀彈 這一話題,提出自己的看法。我認為,在現有的所有體系中,都沒有所謂的 銀彈 銀彈 只是人們想擁有乙個一勞永逸的解決辦法而針對乙個具體事件想出來的臨時的可行的某乙個措施,它的效用時間是有限的,並且解決方法本身並...
和神話裡的主角聊聊天
四月二十二日下午,幾乎永遠陽光燦爛的舊金山天空有些烏雲,我在舊金山最高的標誌性建築transamerica pyramid大樓的,可以看到灣區360度的景致 金門大橋,海灣大橋,海灣,遠處的山,近一點的老式的低矮的小樓,以及刀切出來似的整齊的街道。bv capital 這裡是bv capital的所...