'計算機是如何存儲小數的?在C語言編程中,應該注意什麼?'

"

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

"

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

"

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

計算機是如何存儲小數的?在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 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

"

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

計算機是如何存儲小數的?在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 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

1.0 似乎是沒有辦法存儲的

現在讀者可能又有疑問了,因為 1.0 =1.0×2^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-754 標準,很可能讀者和我使用的平臺正是使用的 IEEE-754 標準。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

計算機是如何存儲小數的?在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 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

1.0 似乎是沒有辦法存儲的

現在讀者可能又有疑問了,因為 1.0 =1.0×2^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浮點數的表示方法了,下面是幾個數字的表示方法:

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

幾個數字的表示方法

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

"

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

計算機是如何存儲小數的?在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 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

1.0 似乎是沒有辦法存儲的

現在讀者可能又有疑問了,因為 1.0 =1.0×2^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浮點數的表示方法了,下面是幾個數字的表示方法:

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

幾個數字的表示方法

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

相等

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

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

"

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

計算機是如何存儲小數的?在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 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

1.0 似乎是沒有辦法存儲的

現在讀者可能又有疑問了,因為 1.0 =1.0×2^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浮點數的表示方法了,下面是幾個數字的表示方法:

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

幾個數字的表示方法

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

相等

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

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

#define EPSILON 1.0e-7
#define flt_equals(a, b) (fabs((a)-(b)) < EPSILON)

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

"

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

計算機是如何存儲小數的?在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 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

1.0 似乎是沒有辦法存儲的

現在讀者可能又有疑問了,因為 1.0 =1.0×2^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浮點數的表示方法了,下面是幾個數字的表示方法:

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

幾個數字的表示方法

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

相等

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

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

#define EPSILON 1.0e-7
#define flt_equals(a, b) (fabs((a)-(b)) < EPSILON)

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

讀者應該自行判斷它是否符合自己的程序

在本例中,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語言程序開發中,一個小技巧是,將整數轉換為浮點數,這樣就方便判斷後續處理是否會造成溢出了。處理完畢後,再將該數轉換回整數即可。

"

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

計算機是如何存儲小數的?在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 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

1.0 似乎是沒有辦法存儲的

現在讀者可能又有疑問了,因為 1.0 =1.0×2^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浮點數的表示方法了,下面是幾個數字的表示方法:

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

幾個數字的表示方法

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

相等

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

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

#define EPSILON 1.0e-7
#define flt_equals(a, b) (fabs((a)-(b)) < EPSILON)

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

讀者應該自行判斷它是否符合自己的程序

在本例中,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語言程序開發中,一個小技巧是,將整數轉換為浮點數,這樣就方便判斷後續處理是否會造成溢出了。處理完畢後,再將該數轉換回整數即可。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

將整數轉換為浮點數,就方便判斷後續處理是否會造成溢出了

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

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

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

double magnitude(double re, double im)
{
return sqrt(re*re + im*im);
}

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

"

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

計算機是如何存儲小數的?在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 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

1.0 似乎是沒有辦法存儲的

現在讀者可能又有疑問了,因為 1.0 =1.0×2^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浮點數的表示方法了,下面是幾個數字的表示方法:

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

幾個數字的表示方法

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

相等

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

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

#define EPSILON 1.0e-7
#define flt_equals(a, b) (fabs((a)-(b)) < EPSILON)

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

讀者應該自行判斷它是否符合自己的程序

在本例中,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語言程序開發中,一個小技巧是,將整數轉換為浮點數,這樣就方便判斷後續處理是否會造成溢出了。處理完畢後,再將該數轉換回整數即可。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

將整數轉換為浮點數,就方便判斷後續處理是否會造成溢出了

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

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

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

double magnitude(double re, double im)
{
return sqrt(re*re + im*im);
}

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

謹防計算中間值溢出

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

double magnitude(double re, double im)
{
double r;
re = fabs(re);
im = fabs(im);
if (re > im) {
r = im/re;
return re*sqrt(1.0+r*r);
}
if (im == 0.0)
return 0.0;
r = re/im;
return im*sqrt(1.0+r*r);
}
"

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

計算機是如何存儲小數的?在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 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

1.0 似乎是沒有辦法存儲的

現在讀者可能又有疑問了,因為 1.0 =1.0×2^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浮點數的表示方法了,下面是幾個數字的表示方法:

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

幾個數字的表示方法

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

相等

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

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

#define EPSILON 1.0e-7
#define flt_equals(a, b) (fabs((a)-(b)) < EPSILON)

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

讀者應該自行判斷它是否符合自己的程序

在本例中,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語言程序開發中,一個小技巧是,將整數轉換為浮點數,這樣就方便判斷後續處理是否會造成溢出了。處理完畢後,再將該數轉換回整數即可。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

將整數轉換為浮點數,就方便判斷後續處理是否會造成溢出了

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

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

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

double magnitude(double re, double im)
{
return sqrt(re*re + im*im);
}

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

謹防計算中間值溢出

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

double magnitude(double re, double im)
{
double r;
re = fabs(re);
im = fabs(im);
if (re > im) {
r = im/re;
return re*sqrt(1.0+r*r);
}
if (im == 0.0)
return 0.0;
r = re/im;
return im*sqrt(1.0+r*r);
}
計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

有效數字丟失

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

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

"

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

計算機是如何存儲小數的?在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 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

1.0 似乎是沒有辦法存儲的

現在讀者可能又有疑問了,因為 1.0 =1.0×2^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浮點數的表示方法了,下面是幾個數字的表示方法:

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

幾個數字的表示方法

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

相等

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

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

#define EPSILON 1.0e-7
#define flt_equals(a, b) (fabs((a)-(b)) < EPSILON)

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

讀者應該自行判斷它是否符合自己的程序

在本例中,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語言程序開發中,一個小技巧是,將整數轉換為浮點數,這樣就方便判斷後續處理是否會造成溢出了。處理完畢後,再將該數轉換回整數即可。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

將整數轉換為浮點數,就方便判斷後續處理是否會造成溢出了

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

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

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

double magnitude(double re, double im)
{
return sqrt(re*re + im*im);
}

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

謹防計算中間值溢出

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

double magnitude(double re, double im)
{
double r;
re = fabs(re);
im = fabs(im);
if (re > im) {
r = im/re;
return re*sqrt(1.0+r*r);
}
if (im == 0.0)
return 0.0;
r = re/im;
return im*sqrt(1.0+r*r);
}
計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

有效數字丟失

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

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

如果兩個比較接近的浮點數相減,這個“隱藏的1”就會被抵消

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

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

經驗法則

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

"

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

計算機是如何存儲小數的?在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 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

1.0 似乎是沒有辦法存儲的

現在讀者可能又有疑問了,因為 1.0 =1.0×2^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浮點數的表示方法了,下面是幾個數字的表示方法:

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

幾個數字的表示方法

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

相等

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

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

#define EPSILON 1.0e-7
#define flt_equals(a, b) (fabs((a)-(b)) < EPSILON)

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

讀者應該自行判斷它是否符合自己的程序

在本例中,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語言程序開發中,一個小技巧是,將整數轉換為浮點數,這樣就方便判斷後續處理是否會造成溢出了。處理完畢後,再將該數轉換回整數即可。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

將整數轉換為浮點數,就方便判斷後續處理是否會造成溢出了

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

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

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

double magnitude(double re, double im)
{
return sqrt(re*re + im*im);
}

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

謹防計算中間值溢出

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

double magnitude(double re, double im)
{
double r;
re = fabs(re);
im = fabs(im);
if (re > im) {
r = im/re;
return re*sqrt(1.0+r*r);
}
if (im == 0.0)
return 0.0;
r = re/im;
return im*sqrt(1.0+r*r);
}
計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

有效數字丟失

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

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

如果兩個比較接近的浮點數相減,這個“隱藏的1”就會被抵消

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

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

經驗法則

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

C語言如此簡單

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

不要忘記整數

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

"

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

計算機是如何存儲浮點數的呢?

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

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

計算機是如何存儲小數的?在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 通過將零作為一種特殊情況來解決這一問題:如果數字的每一位都為零,那麼數字就被認為是零。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

1.0 似乎是沒有辦法存儲的

現在讀者可能又有疑問了,因為 1.0 =1.0×2^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浮點數的表示方法了,下面是幾個數字的表示方法:

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

幾個數字的表示方法

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

相等

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

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

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

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

#define EPSILON 1.0e-7
#define flt_equals(a, b) (fabs((a)-(b)) < EPSILON)

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

讀者應該自行判斷它是否符合自己的程序

在本例中,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語言程序開發中,一個小技巧是,將整數轉換為浮點數,這樣就方便判斷後續處理是否會造成溢出了。處理完畢後,再將該數轉換回整數即可。

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

將整數轉換為浮點數,就方便判斷後續處理是否會造成溢出了

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

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

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

double magnitude(double re, double im)
{
return sqrt(re*re + im*im);
}

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

謹防計算中間值溢出

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

double magnitude(double re, double im)
{
double r;
re = fabs(re);
im = fabs(im);
if (re > im) {
r = im/re;
return re*sqrt(1.0+r*r);
}
if (im == 0.0)
return 0.0;
r = re/im;
return im*sqrt(1.0+r*r);
}
計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

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

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

有效數字丟失

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

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

如果兩個比較接近的浮點數相減,這個“隱藏的1”就會被抵消

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

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

經驗法則

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

C語言如此簡單

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

不要忘記整數

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

計算機是如何存儲小數的?在C語言編程中,應該注意什麼?

點個贊再走吧

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

"

相關推薦

推薦中...