每個C語言程式設計師都應該明白,計算機究竟是如何存儲小數的?

2021-01-08 IT劉小虎

浮點型在內存中的存儲分布方式因機器平臺而異,完全理解所有機器平臺中的浮點型存儲無疑是一件相當麻煩的事。幸運的是,大多機器平臺都遵守 IEEE-754 標準,很可能讀者和我使用的平臺正是使用的 IEEE-754 標準。

計算機如何存儲浮點數

IEEE-754是如何存儲浮點數的?

IEEE-754浮點(32位)或雙精度(64位)有三個部分(在IEEE-854下也有類似的96位擴展精度格式):符號位,表示數字是正的還是負的;指數位;以及指定實際數字的尾數位。以C語言中的單精度浮點數為例,下面是某位浮點數的位布局:

某位浮點數的位布局

該浮點數的值等於尾數乘以2^x。讀者應該注意,上圖是二進位分數,因此0.1表示1/2。為了方便理解,我們可以將其與十進位的小數對應起來:十進位的0.1等於1*10^-1,所以二進位的0.1等於1*2^-1,也即1/2。

「尾數+指數」模式存儲浮點數可能有一點問題,例如:2x10^-1=0.2x10^0=0.02x10^1,以此類推。同樣一個數字可能有多種「尾數+指數」的表示方法,而同時兼顧多種表示方法勢必會造成巨大的浪費(也可能使在硬體中實現數學操作變得困難和緩慢)。

同時兼顧多種表示方法勢必會造成巨大的

所以,「尾數+指數」的存儲模式需要一個統一的標準。事實上,IEEE-754 確實已經有標準了:假設給定一個二進位的浮點數,那麼除非這個數是 0,否則總有某個位是 1。將小數點移到第一個 1 之後,調整指數位,這樣一來,「尾數+指數」的唯一存儲方式就固定下來了,也即「1.m x 2^n」形式。

既然小數點前總是 1,那麼上述標準下的「尾數+指數」的存儲模式甚至都不需要再花費空間存儲小數點前的 1.

但是如果數字是零呢?IEEE Standards Committee 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

現在讀者可能又有疑問了,因為 1.0 =1.0×2^0,上述存儲模式不存儲小數點前的 1,也即尾數和指數部分都為 0,而「如果數字的每一位都為零,那麼數字就被認為是零」,這樣看來,1.0 似乎是沒有辦法存儲的。

1.0 似乎是沒有辦法存儲的

當然可以存儲 1.0。單精度浮點數的指數部分是「shift-127」編碼的,也即實際的指數等於 eeeeee 減去 127,所以 1.0 的表示方法實際上是 1.0×2^127。同樣的道理,最小值本應該是 2^-127,按照「shift-127」編碼指數部分,也即 2^0,可是這樣又變成「指數部分和尾數部分都為零」了,因此在該標準下的最小值,實際上的寫法是 2^1,也即 2^-126。

在我看來,為了表示 0 和 1,捨棄最小值(2^-127)是非常可取的做法。

零不是唯一的「特殊情況」。對於正無窮大和負無窮大,非數字(NaN),以及沒有數學意義的結果(例如,非實數,或無窮大乘以零之類的計算結果)也有表示:如果指數的每一位都等於1,那麼這個數字是無窮大,如果指數的每一位都等於1,並且尾數位也都等於1,那麼這個數字就是NaN。符號位仍然區分+/-inf和+/-nan。

現在,讀者應該明白IEEE-754浮點數的表示方法了,下面是幾個數字的表示方法:

幾個數字的表示方法

作為程式設計師,了解浮點表示的某些特性是很重要的,下標列出了單精度和雙精度IEEE浮點數的示例值:

單精度和雙精度IEEE浮點數的示例值

注意,本文中的所有數字都假定為單精度浮點數;上面包含雙精度浮點數用於參考和比較。

在C語言程序開發中,數值的處理是一門值得深究的科學。本文不可能將複雜的數值算法以及相關的C語言程序開發經驗一一列出。事實上,討論如何以理想的數值精度進行計算,就和討論如何編寫最快的C語言程序,如何設計一款優秀的軟體一樣,主要取決於程式設計師本身的綜合素質。

鑑於此,這裡將嘗試介紹一些基礎的,我認為每個C語言程式設計師都應該知道的內容。

相等

首先,我們應該明白C語言程序開發中的兩個浮點數何時相等。可能讀者並不覺得難,因為似乎C語言中的 == 運算符就能判斷兩個浮點數是否完全相等。

然而實際上,C語言中的 == 運算符是逐位比較兩個操作數的,而兩個浮點數的精度總是有限的,在這種場景下,== 運算符的實際使用意義就沒有那麼大了。

== 運算符的實際使用意義就沒有那麼大

讀者應該已經明白,計算機存儲浮點數時,很有可能是需要捨棄一些位的(如果該浮點數過長),如果 CPU 或者相應的程序沒有按照預期四捨五入,那麼使用 == 運算符判斷兩個浮點數是否相等可能會失敗。

例如,標準C語言函數庫三角函數 cos() 的實現其實只是一種多項式近似,也就是說,我們並不能指望 cos(π/2) 結果的每一個位都為零。在C語言程序開發中,我們甚至不能準確的表示 π。

看到這裡,讀者應該思考「相等到底是什麼意思呢?」,對於大多數情況來說,兩個數「相等」意味著這兩個數「足夠接近」。本著這種精神,在實際的C語言程序開發中,程式設計師通常定義一個很小的值模擬「足夠接近」,並以此判斷兩個浮點數是否「足夠接近到相等」,例如:

判斷兩個浮點數是否「足夠接近到相等」

宏 flt_equals(a, b) 正是通過判斷 a 和 b 的距離是否小於 EPSILON(10的-7次方),來斷定 a 和 b 是否可以被認為「相等」的。這樣的近似模擬技術有時候是有用的,有時候卻可能導致錯誤結果,讀者應該自行判斷它是否符合自己的程序。

在本例中,EPSILON 可以看作是一種用於說明程序精度的標尺。應該明白,衡量精度的應該是有效數字,糾結 EPSILON 的具體大小並無意義,下面是一個例子。

假設在某段C語言程序中有兩個數字 1.25e-20 和 2.25e-20,它倆的差值是 1e-20,遠小於 EPSILON,但是顯然它倆並不相等。但是如果這兩個數字是 1.2500000e-20和1.2500001e-20,那麼就可以認為它們是相等的。也就是說,兩個數字距離足夠接近時,我們還需要關注需要匹配多少有效數字。

還需要關注需要匹配多少有效數字

溢出

計算機存儲空間總是有限的,因此數值溢出是C語言程式設計師最關心的問題之一。讀者應該已經知道,如果向C語言中的最大無符號整數加一,該整數將歸零,令人崩潰的是,我們並不能只通過看這個數字的方式獲知是否有溢出發生,歸零的整數看起來和標準零一模一樣。

當溢出發生時,實際上大多數 CPU 是會設置一個標誌位的,如果讀者懂得彙編,可以通過檢查該標誌位獲知是否有溢出發生。

float 浮點數溢出時,我們可以方便的使用 +/- inf(無窮)。+inf(正無窮)大於任何數字,-inf(負無窮)小於任何數字,inf+1 等於 inf ,依此類推。因此在C語言程序開發中,一個小技巧是,將整數轉換為浮點數,這樣就方便判斷後續處理是否會造成溢出了。處理完畢後,再將該數轉換回整數即可。

不過,將整數轉換為浮點數判斷是否溢出也是要付出代價的,因為浮點數可能沒有足夠的精度來保存整個整數。32 位的整數可以表示任何 9 位十進位數,但是 32 位的浮點數最多只能表示 7 位的十進位數。所以,如果將一個很大的整數轉換為浮點數,可能不會得到期望的結果。

此外,在C語言程序開發中,int 與 float 之間的數值類型轉換,包括 float 與 double 之間的數值類型轉換,實際上是會帶來一定的性能開銷的。

讀者應該明白,在C語言程序開發中,不管是否使用整數,都應該小心避免數值溢出的發生,不僅僅是最開始和最終結果數值可能溢出,在一些計算的中間過程,可能會產生一些更大的值。一個經典的例子是「C語言數字配方」計算複數的幅度問題,極可能造成數值溢出的C語言實現是下面這樣的:

極可能造成數值溢出的C語言實現

假設該複數的實部 re 和虛部 im 都等於 1e200,那麼它們的幅度約為 1.414e200,這的確在雙精度的允許範圍內。但是,上述C語言代碼的中間過程將產生 1e200 的平方值,也即 1e400,這超出了 inf 的範圍,此時上面的實現函數計算的平方根將仍然是無窮大。

因此,magnitude() 函數的更佳C語言實現如下:

magnitude() 函數的更佳C語言實現

應該明白,上述C語言代碼為了避免數值溢出,給出的實現實際上是一種近似。例如 im 等於 1e200,re 等於 1,那麼 im/re 的平方仍然能夠達到 1e400,這會造成數值溢出。但是平方 re/im 卻是可以的,因為 1e-400 會被四捨五入到零,足夠接近得到正確的答案。

有效數字丟失

上面討論的浮點數精度,以及相等問題只是C語言程序數值運算中的冰山一角。「有效數字丟失」指的是一類情況,在這種情況下,程式設計師很可能丟失數值的準確性。

前面我們提到,1.m 的尾數形式確保小數點前總是 1(非零時),既然如此,我們沒有必要再花費一個位的空間用於存儲 1。此時,即使尾數只有最後一位為 1,其他位都為 0,那麼它的有效數字也是全部的,因為最前方有個「隱藏的 1」。但是,如果兩個比較接近的浮點數相減,這個「隱藏的1」就會被抵消,最終得到的答案可能只剩下一位有效數字的精度。

幸運的是,就像上面求複數幅度避免出現數值溢出一樣,避免兩個接近的數字相減出現精度損失的方法也是有的,但是並沒有一個通用的方法。這裡給出一個簡單的實例就是使用 1/x 的函數代替 x 的函數,這對於處理二次運算很有效。我的建議是,如果讀者發現自己的C語言程序給出了令人懷疑的數值,就應該檢查一下相應的減法運算了。

避免有效數字丟失帶來的問題

看到這裡,相信讀者應該想到C語言程序中的加法可能也有同樣的問題:假設有數字 1.0,現在將其與 1e-20 相加。程序很可能認為 1e-20 很小,小於 EPSILON,於是忽略它,得到答案 1.0。這實際上也是一種精度損失。

經驗法則

要完全規避C語言程序中的浮點數可能帶來的問題,工作量無疑是巨大的。為了簡化問題,我們通常認為浮點數帶來的精度問題是這樣的:對浮點數的操作越多,損失的精度也會越多。

正如前文舉的例子 cos(π/2),C語言它的實現實際上是一種近似,得到的答案並不是 0,而是 6.12303E-17。不過就這個答案本身而言,它已經足夠小,認為等於 0 也沒什麼大問題。但是如果我們下一步計算是除以 1e-17,那麼得到的答案就約是 6 了,這與預期的零相差甚遠。

不要忘記整數

最後,在C語言程序開發中,並不是只有浮點數才重要的,整數同樣重要,它的精確性是一個有用的工具。有時程序需要跟蹤變化的分數(例如比例因子)。在這種情況下,既然浮點數受各種因素影響,那麼我們完全可以將該分數存儲為整數分子和分母來避免問題。在需要使用浮點數時,隨時再做一次除法運算就可以了。

點個讚再走吧

歡迎在評論區一起討論,質疑。文章都是手打原創,每天最淺顯的介紹C語言、linux等嵌入式開發,喜歡我的文章就關注一波吧,可以看到最新更新和之前的文章哦。

相關焦點

  • 計算機二級c語言
    計算機二級c語言,那麼,關於計算機二級c語言還有哪些知識點呢? 成人計算機培訓機構市面上非常多,尤其是我們剛剛從學校走出來不知道哪家成人計算機培訓機構比較好,自己隨便在網上搜索了一下成人計算機培訓機構哪家好?
  • 很多C語言程式設計師都知道「大小端」,但並不知道何時才應該考慮它
    上一節討論了C語言的 union 語法的一個使用場景,讀者應該發現,union 成員共享一塊內存的特性使得C語言程式設計師能夠寫出更加節約資源的程序。不過也有讀者在看了上一節文章後,評論或者私信說了一些關於C語言 union 的特性,這些回復有些是不嚴謹的。如果不能完全掌握 union 語法,在使用時難免會覺得困惑,甚至跳入「陷阱」,寫出有隱患的C語言代碼。
  • 每個程式設計師都應該嘗試的項目
    一年前,我發表了「每個程式設計師都應該嘗試的項目」一文。文中列出了每個程式設計師都應該去嘗試的項目,包括一個文本編輯器、太空入侵者遊戲、一個 BASIC 編譯器、一個小型的作業系統、一個電子表格和一個視頻遊戲控制臺模擬器。這篇文章在網上爆紅,一個月內瀏覽量超過 10 萬次。
  • 陝西計算機二級c語言培訓班
    陝西計算機二級c語言培訓班,市面上的培訓機構越來越多,甚至有些新成立的小機構大力宣傳,結果只有一兩個老師和幾個市場人員,遇到這樣的計算機培訓機構需要擦亮雙眼,這樣的培訓機構既保證不了學到的內容,也保證不了就業。
  • 很多C語言初學者都非常好奇的問題,怎樣定義可以可變參數函數?
    而其他C語言函數則不同,它們大都有固定數量的參數(0個,3個等),調用這些函數必須傳遞對應數目的參數。有些持有「特殊論」的初學者認為像 printf() 這樣的「可變參數」函數是「特殊的」,是系統定義的,我們程式設計師只能定義固定參數的函數,其實不是的,C語言是有手段定義自己「可變參數」函數的。printf() 究竟是不是只能由系統定義的「特殊」函數呢?
  • 豆瓣9.0,百萬程式設計師的寶藏書,C語言進階必看
    想成為一名合格的C語言程式設計師,這些你都了解了嗎?三:字符串、字符和字節字符串是一種重要的數據類型,但是C語言並沒有顯式的字符串數據類型,因為字符串以字符串常量的形式出現或者存儲於字符數組中。字符串常量很適用於那些程序不會對它們進行修改的字符串。
  • 想要成為IT大佬都應該看的12本C語言書目
    本書是一本優秀的編譯器構造方面的教材,適合於高等院校計算機專業的學生和使用C語言的專業程式設計師。編程將會改變我們的思維,教會我們如何思考,讓我們的思維插上計算機的翅膀,以一種全新的方式來看世界。現在就讓我們一起走進計算機編程的神奇世界,探索和發現計算機編程的魔力吧。
  • 國外程式設計師推薦:每個程式設計師都應讀的書
    源於2008年8月4日,StackOverflow 網友 Bert F 發帖提問:哪本最具影響力的書,是每個程式設計師都應該讀的?「如果能時光倒流,回到過去,作為一個開發人員,你可以告訴自己在職業生涯初期應該讀一本, 你會選擇哪本書呢?我希望這個書單列表內容豐富,可以涵蓋很多東西。」
  • 如何快速學習計算機程式語言
    如何快速學習計算機程式語言呢?這是很多零基礎同學想要知道的答案。隨著網際網路的飛速發展,程式設計師的工資越來越高,很多人想加入程式設計師的隊列,想要快速掌握編程技術。那麼應該如何快速學習計算機程式語言呢?1、首先應該知道自己為什麼學習計算機程式語言,如果只是盲目的追求高薪,對電腦程式沒有濃厚的興趣,那麼零基礎想要快速學習計算機程式語言是非常困難的。想計算機程式語言,應該擁有濃厚的興趣,再加上堅持不懈的精神,這樣才能更快地學習程式語言。
  • 指針為什麼被譽為C語言靈魂呢?
    (過於自信「指針存儲的是變量的內存地址」這句話應該任何講 C 語言的書都會提到吧。、每個住戶都分配一個門牌號:301、302、403、404、501......同樣,在計算機中,我們也要保證給每一個 byte 的編號都是唯一的,這樣才能夠保證每個編號都能訪問到唯一確定的 byte。
  • 為什麼指針被譽為 C 語言靈魂?
    相比之下,那時候最喜歡 Java,在 Java 裡隨便怎麼寫都不會發生類似的異常,頂多偶爾來個 PointerException,也是比較好排查的。直到後來對內存和指針有了更加深刻的認識,才慢慢會用 C 寫上千行的項目,也很少會再有內存問題了。「指針存儲的是變量的內存地址」這句話應該任何講 C 語言的書都會提到吧。
  • 貴州計算機二級c語言培訓
    貴州計算機二級c語言培訓,那麼,關於計算機二級c語言培訓還有哪些知識點呢? 在90年代的童年時代,每個人都仍在使用諾基亞並收聽MP3。當他們到達高中時,他們發現班上的一些學生開始使用智慧型手機。對於00以後出生的人,大概在初中就開始玩ipad。現在出去,看著公共汽車上用iwatch的學生,我不得不感嘆時代在進步。
  • C語言程式設計師未來的發展前景如何
    雖然C語言並沒有直接參與到網際網路和移動網際網路領域的開發中,包括Web開發、Android開發等領域,而且在大數據領域,C語言也並不是常見的解決方案,但是在作業系統開發、容器開發和嵌入式開發領域,C語言依然具有大量的應用場景,而且未來較長一段時間內,C語言依然是IT行業內最為流行的程式語言之一。
  • C語言未來會如何發展,你怎麼看?
    程式語言是另一種程式語言的基礎,因此,它被稱為所有編程的「母語」。C語言的使用C語言是一種非常簡單的語言,通常被全世界的軟體工程師使用。最初,這種語言是用於系統開發,確切地說是構成作業系統的程序。C程式語言的優勢很大程度上可以生成與彙編語言編寫的代碼幾乎一樣快的運行速度。
  • 不一樣的C/C+程式語言的學習策略,程式設計師都是寫出來的!
    現有C/C++語言的教材,大部分是教授和講師寫出來的,在身經百戰的程式設計師面前,沒有經過實戰的教授和講師始終停留在小學生水平。 網上也有不少視頻教程,大部分的教程只是把書本上的內容搬到視頻中,沒有新意。
  • 不同語言的程式設計師不要再相互鄙視了!
    =======華麗的分界線========= 程式設計師們都有一種自命不凡的氣質,總認為自己是最牛的,其他人都是垃圾,不同的計算機語言之間也存在著鄙視鏈,今天就來盤點一下,這其中的一些事情,看看能否從中得出什麼結論。
  • C語言編程習慣心得
    注釋是寫給程式設計師看的,不是寫給電腦看的。所以注釋的內容,C語言編譯器在編譯時會被自動忽略。C語言注釋方法有兩種:多行注釋: /* 注釋內容 */單行注釋: //注釋一行以下是使用多行注釋與單行注釋的例子:c語言的命名編程時給變量或者函數起的名字就是標識符,就好比每一位同學都有姓名,姓名就是這位同學的標識符。C語言的標識符是不可以隨便起名字的,必須遵守一定的規則。
  • 零基礎的同學應該如何選擇計算機入門語言呢?
    如果你是有Java基礎的,那你應該去找一個Java的基礎教程鞏固自己的Java基礎,在鞏固基礎的同時多實踐,多敲代碼,很多時候你覺得你寫完了,運行出來的結果和自己的想法應該一模一樣,但是有時候你發現結果和你的想法截然不同的時候,並且去思考的時候,這個時候你才算剛剛入門了,起碼你現在對自己的代碼有思考了。
  • 學計算機編程開發:應該學習哪一種程式語言,C+還是C#?
    因此,對於您來說,確定自己作為程式設計師的目標很重要,以便針對技術做出正確的選擇。但是,您很難做出這個決定,因為您對這一領域的理解還不夠深入。如果您願意被Microsoft生態系統所束縛,則絕對應該選擇C#,而使用C ++則很可能會開發桌面應用程式。 我不明白的是為什麼您要精確列出這兩種語言?還有許多其他選擇,例如C,Java,Ruby,PHP,Python等。
  • 這該死的 C 語言
    C 語言廣泛應用於底層開發,C 語言在計算機體系中佔據著不可替代的作用,可以說 C 語言是編程的基礎,也就是說,不管你學習任何語言,都應該把 C 語言放在首先要學的位置上。,這就意味著,對於在一臺計算機上編寫的 C 語言程序可以在另一臺計算機上輕鬆地運行,從而極大的減少了程序移植的工作量。