很久以前,準(zhǔn)確點(diǎn)說應(yīng)該是4年前的一個(gè)下午,程序員成心文同學(xué)遇到了他編程史上的第一個(gè)浮點(diǎn)數(shù)誤差bug,這個(gè)bug正如標(biāo)題所述,在他的程序下2.55*100的結(jié)果竟是254.99999999999997,而非255。因?yàn)槭浅醮斡龅剑运膊恢朗窃趺椿厥?,但在請教中外倆.NET大牛無果后,他果斷拍板決定“自己動(dòng)手,豐衣足食”。
后來他終于找到了原因,簡單地說:計(jì)算機(jī)將小數(shù)部分0.55轉(zhuǎn)換成二進(jìn)制時(shí)會(huì)形成一個(gè)“0011”組合的無限循環(huán),從而造成誤差。解決辦法是盡量用decimal類型替換double類型。
這位年輕的程序員將他的這次編程經(jīng)歷記錄在了微博上,一不小心被小編扒了出來,于是IT之家就有了這篇關(guān)于“世界上只有10種人,一種懂二進(jìn)制、另一種不懂”的文章。
當(dāng)然,除了上面的這位同學(xué)這之外,小編還發(fā)現(xiàn)一位名叫周花卷的大牛也遇到了類似的問題,他在谷歌Chrome瀏覽器的開發(fā)者控制臺(Win環(huán)境下F12/Mac環(huán)境按Command+Option+I可以打開一個(gè)“開發(fā)者工具”窗口,然后點(diǎn)上面的“Console”標(biāo)簽,你會(huì)看到一個(gè)控制臺窗口)里輸入0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1”(一共10個(gè)0.1),按下回車后結(jié)果竟是0.99999999999999,這個(gè)問題同樣也是二進(jìn)制造成的。
OK,下面進(jìn)入正題,小編接下來要用更專業(yè)的解釋來帶大家飛,不懂二進(jìn)制的也可以學(xué)習(xí)一下哈。
要搞清楚這個(gè)問題,我們先來理解一下進(jìn)制的概念。在十進(jìn)制中,一位數(shù)字我們可以使用0到9,比9多的時(shí)候就會(huì)變成10,這就是一個(gè)兩位數(shù)了,也就是進(jìn)位了,二進(jìn)制也是一樣,只不過一位數(shù)字只能使用0和1,再多就要進(jìn)位了。那么進(jìn)制的本質(zhì)又是什么呢?我們隨便拿一個(gè)十進(jìn)制的數(shù)字來看一看:
看懂了沒?一個(gè)十進(jìn)制數(shù)實(shí)際上就是其中每一位數(shù)依次乘以10的0、1、2…次冪(權(quán)重),然后再把結(jié)果加起來,那么以此類推,二進(jìn)制里面就是把上面的10換成2唄?我們來看一個(gè):
于是二進(jìn)制數(shù)1001也就是十進(jìn)制數(shù)的9。到這里似乎還沒什么問題,因?yàn)槲覀冎挥懻摿苏麛?shù)呢,每一個(gè)十進(jìn)制整數(shù)都可以轉(zhuǎn)換成一個(gè)二進(jìn)制整數(shù),反過來,每一個(gè)二進(jìn)制整數(shù)也都可以轉(zhuǎn)換成一個(gè)十進(jìn)制整數(shù)。不過,如果把小數(shù)也加進(jìn)來呢?先看一個(gè)十進(jìn)制的小數(shù):
看懂了沒?其實(shí)就是把10上面的指數(shù)變成了負(fù)數(shù)而已,不難吧。那么以此類推,二進(jìn)制的小數(shù)也就是把10換成2唄,我們來看一個(gè):
上面我們理解了進(jìn)制的一些本質(zhì)特性,算不過來也沒關(guān)系,我們暫且先不管它,不過,這跟我們遇到的問題到底有什么關(guān)系?別急,我們再看一下當(dāng)引入小數(shù)之后,進(jìn)制之間的轉(zhuǎn)換到底出了什么bug。我們知道實(shí)數(shù)的數(shù)軸是連續(xù)的,每兩個(gè)數(shù)字之間的部分是可以被無限分割的,舉個(gè)例子,0和1之間的這部分,如果用十進(jìn)制一位小數(shù)來分割的話,可以分成10份,也就是0.1、0.2、0.3……0.9、1,如果用二進(jìn)制一位小數(shù)來分割的話,則只能分成兩份,也就是0.1(十進(jìn)制的0.5)、1。你發(fā)現(xiàn)了什么問題?無論小數(shù)點(diǎn)后面增加多少位數(shù)字,二進(jìn)制永遠(yuǎn)只能以2來分割數(shù)軸,而十進(jìn)制則是以10來分割數(shù)軸。
讓我們回想一下小學(xué)的數(shù)學(xué)知識,在十進(jìn)制中,如果要用有限小數(shù)來表示一個(gè)分?jǐn)?shù)的值,那么這個(gè)分?jǐn)?shù)的分母(化簡之后)一定不能包含除了2和5以外的其他質(zhì)因數(shù),因?yàn)槭M(jìn)制以10來分割數(shù)軸,而10分解質(zhì)因數(shù)的結(jié)果為2×5。舉個(gè)例子:1/8、1/10、1/25都可以換算成有限小數(shù)(分別是0.125、0.1、0.04),因?yàn)檫@些分?jǐn)?shù)的分母分解質(zhì)因數(shù)之后只包含2或者5(8=2×2×2、10=2×5、25=5×5),而當(dāng)分母包含其他質(zhì)因數(shù)時(shí),例如1/3、1/7、1/18這些則無法用有限小數(shù)來表示(也就是俗話說的“除不盡”)。如果我們把這個(gè)規(guī)律套用到二進(jìn)制上會(huì)怎么樣呢?2本身就是一個(gè)質(zhì)數(shù),無法分解質(zhì)因數(shù)了,因此在二進(jìn)制中,如果要用有限小數(shù)來表示一個(gè)分?jǐn)?shù)的值,那么這個(gè)分?jǐn)?shù)的分母一定只能包含2這一個(gè)質(zhì)因數(shù),換句話說,分母必須為2的冪(2、4、8、16、32……)。
好了,我們回頭看看開頭的題目,0.1換算成分?jǐn)?shù)就是1/10,而1/10的分母是10,10并不是2的冪,因此,在二進(jìn)制中并不能用有限小數(shù)來表示1/10這個(gè)值。事實(shí)上,如果將0.1轉(zhuǎn)換成二進(jìn)制,我們會(huì)得到一個(gè)無限循環(huán)小數(shù):0.000110011001100……看到這里,很多人估計(jì)已經(jīng)想明白了,沒錯(cuò),計(jì)算機(jī)的精度是有限的,并不能直接處理無限小數(shù),對于無限小數(shù)必須要截短到某個(gè)位置把它變成有限小數(shù),但截短之后這個(gè)數(shù)就不準(zhǔn)了,必然就產(chǎn)生了一點(diǎn)誤差,而連續(xù)加10次會(huì)將這種誤差放大,當(dāng)誤差被放大到一定程度時(shí),計(jì)算的結(jié)果就會(huì)出問題了,于是我們就看到了開頭的那一幕。如果用十進(jìn)制來類比的話,大家可以想象一下,1/3+1/3+1/3=1,但1/3只能用無限循環(huán)小數(shù)來表示,即0.333333……,如果我們將它截短到某一位,假設(shè)截到0.333,那么0.333+0.333+0.333=0.999,你看,同樣也會(huì)出問題。
問題的原因總算搞清楚了,不過感覺很坑爹啊,計(jì)算機(jī)居然算不準(zhǔn)小數(shù),但為什么平時(shí)大家很少因此遇到問題呢?那是因?yàn)榇蠖鄶?shù)用戶都不用編寫程序,但對于整天編寫程序的程序員來說,這樣的問題其實(shí)經(jīng)常遇到。比如說,如果你在一段程序中需要讓計(jì)算機(jī)判斷0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1是否等于1,計(jì)算機(jī)會(huì)告訴你不等于1,坑爹吧?如果這段程序涉及到算錢,有時(shí)候就會(huì)錯(cuò)得很離譜,因此有經(jīng)驗(yàn)的程序員在碰到小數(shù)計(jì)算的時(shí)候都會(huì)特別小心。當(dāng)然,作為一般用戶我們平時(shí)根本不需要關(guān)心這樣的問題,不過計(jì)算機(jī)居然會(huì)算錯(cuò)數(shù),怎么想都覺得挺奇妙的吧?
廣告聲明:文內(nèi)含有的對外跳轉(zhuǎn)鏈接(包括不限于超鏈接、二維碼、口令等形式),用于傳遞更多信息,節(jié)省甄選時(shí)間,結(jié)果僅供參考,IT之家所有文章均包含本聲明。