浮点型数据在内存中是如何存储的
🏖?作者:@malloc不出對象
?專欄:《初識C語言》
👦個人簡介:一名雙非本科院校大二在讀的科班編程菜鳥,努力編程只為趕上各位大佬的步伐🙈🙈
目錄
- 前言
- 一. 浮點型數(shù)據(jù)如何存儲的規(guī)則(理論部分)
- 二. 浮點數(shù)怎么轉化為二進制
- 三. 浮點型數(shù)據(jù)在內存中的存儲
- 四. 關于浮點型經(jīng)常出現(xiàn)的錯誤
- 五. 浮點型數(shù)據(jù)與"零值"的比較
- 六. 個人對于浮點型的看法和做題經(jīng)驗
前言
本篇文章博主將給大家講講浮點型數(shù)據(jù)在內存中的存儲,關于浮點型這個地方其實有很多細節(jié)是需要我們去注意的,也是我們經(jīng)常容易出現(xiàn)錯誤的地方,在這篇文章中我都會給大家總結出來,讓你對浮點型不再感到疑惑。
一. 浮點型數(shù)據(jù)如何存儲的規(guī)則(理論部分)
根據(jù)國際標準IEEE(電氣和電子工程協(xié)會)754,任意一個二進制浮點數(shù)V可以表示成下面的形式:
V = (-1) ^ S * M + 2^E。
S表示符號位,當s = 0,V為正數(shù),當s = 1,V為負數(shù)。
階碼部分(E)(指數(shù)部分),2^E(表示指數(shù)位)
M表示有效數(shù)字,大于等于1,小于2,?浮點數(shù)的精度就是由尾數(shù)來決定的。
IEEE 754規(guī)定:對于32位的浮點數(shù),最高位是符號位s,接著的8位是指數(shù)E,剩下的23位為有效數(shù)字M,如下圖所示:
對于64位的浮點數(shù),最高位是符號位S,接著的11位是指數(shù)E,剩下的52位為有效數(shù)字M,如下圖所示:
IEEE 754對有效數(shù)字M和指數(shù)E,還有一些特別規(guī)定。
前面說過, 1≤M<2 ,也就是說,M可以寫成 1.xxxxxx 的形式,其中xxxxxx表示小數(shù)部分。
IEEE 754規(guī)定,在計算機內部保存M時,默認這個數(shù)的第一位總是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的時候,只保存01,等到讀取的時候,再把第一位的1加上去。這樣做的目的,是節(jié)省1位有效數(shù)字。以32位浮點數(shù)為例,留給M只有23位,將第一位的1舍去以后,等于可以保存24位有效數(shù)字。
至于指數(shù)E,情況就比較復雜。
首先,E為一個無符號整數(shù)(unsigned int)這意味著,如果E為8位,它的取值范圍為0\~255;如果E為11位,它的取值范圍為0~2047。但是,我們知道,科學計數(shù)法中的E是可以出現(xiàn)負數(shù)的,但E為無符號整數(shù)不存在符號位,所以IEEE 754規(guī)定,存入內存時E的真實值必須再加上一個中間數(shù),使其變?yōu)橐粋€正整數(shù),對于8位的E,這個中間數(shù)是127;對于11位的E,這個中間數(shù)是1023。比如,2^-1的E是-1,所以保存成32位浮點數(shù)時,必須保存成-1+127=126,即01111110注意這個地方為存儲值而非真實值.
好了,講了這么多什么約定,那接下來我們來看看浮點數(shù)是怎么化為二進制的。
二. 浮點數(shù)怎么轉化為二進制
首先我們來個簡單的例子:
把十進制小數(shù)5.25化為二進制小數(shù),我們應該怎么操作?
我們分為以下幾步:
1. 以小數(shù)點為界進行拆分;
2. 整數(shù)部分轉為二進制相信大家肯定沒問題
3. 小數(shù)部分采用的是"乘2取整法",當乘2之后小數(shù)部分得到0就停止計算
4. 合并結果:整數(shù)部分 + 小數(shù)部分,最終得到二進制結果為101.01.
我們來進行檢驗一下,發(fā)現(xiàn)確實如我們計算的這樣:
以上就是浮點數(shù)化為二進制的步驟了,下面我們來看看更復雜一點的例子:
把十進制3.14化為二進制:
這里我就不帶大家計算下去了,大家可以看下下圖:
三. 浮點型數(shù)據(jù)在內存中的存儲
我們先來看這個例子,大家想想浮點型數(shù)據(jù)是如何存進去的?跟整型數(shù)據(jù)比有什么區(qū)別?
#include<stdio.h> int main() {float f = 5.5;return 0; }我們來分析一下:
這里的0.5化為二進制就是1 * 2^ -1
先化為二進制—>101.1
再化為標準形式V = (-1)^0 * 1.011 * 2^2;
s = 0, M = 1.011, E = 2;
E + 127 = 129—>10000001, M = 011 0000000000 0000000000
最后以0100 0000 1011 0000 0000 0000 0000 0000存入
化為十六進制為0x40b00000
我們來檢測一下是否如上述所計算得出的答案:
我們發(fā)現(xiàn)確實是如上圖所計算得出的答案,在VS下采用的是小端模式因此是倒著存進去的。
為什么說E的情況比較復雜,其實存的時候E分為三種情況:
E不全為0或不全為1
這時,浮點數(shù)就采用下面的規(guī)則表示,即指數(shù)E的計算值減去127(or 1023),得到真實值,再將有效數(shù)字M前加上第一位的1。比如:0.5(1/2)的二進制形式為0.1,由于規(guī)定正數(shù)部分必須為1,即將小數(shù)點右移1位,則為1.0*2^(-1),其階碼為-1+127=126,表示為01111110,而尾數(shù)1.0去掉整數(shù)部分為0,補齊0到23位00000000000000000000000,則其二進制表示形式為0 01111110 00000000000000000000000,這是屬于正常情況。
E為全0
這時,浮點數(shù)的指數(shù)E等于1~127(or 1~1023)即為真實值,有效數(shù)字M不再加上第一位的1,而是還原為0.xxxxxx的小數(shù)。這樣做是為了表示±0,以及接近于0的很小的數(shù)字。
如果E為全0,此時的E為存儲值,那么想一下E的真實值是不是為-127呢? 如果我們還原回去V = (-1)^ s * 1.xxxxx * 2^ -127,那么這是不是一個非常小的數(shù)呢,它趨向于±0 。此時就有了上面的規(guī)定。
E為全1
這時,如果有效數(shù)字M全為1,表示±無窮大(正負取決于符號位s)。
如果E為全1,此時存儲值為255,減去127即為E的真實值,E = 128,這時如果我們把它還原回去V = (-1)^s * 1.xxxxxx * 2^128/1024;那么此時將為一個正負無窮大的數(shù)字。
好了,關于浮點型的存儲規(guī)則就講到這兒了,下面我們來看一道例題:
這里就來詳細剖析一下這個結果,大家跟著我來分析一遍:
首先我們?yōu)閚開辟4個字節(jié)大小的空間,&n代表的類型為int* 型,我們進行強制類型轉換使它變?yōu)閒loat*型。
int n = 9,表示n以整型的形式進行存儲,當%d站在它的角度進行讀取時,認為它是以整形的存儲方式存放在內存中的,就按照整型的讀取方式打印出來,n為正數(shù)它的原反補碼一致,所以補碼就為00000000 00000000 00000000 00001001,轉化為十進制結果就為9。
當站在%f它的角度進行讀取時,認為它是以浮點型的存儲方式存放在內存中的,就按照浮點型的讀取方式打印。
00000000 00000000 00000000 00001001---->0 00000000 00000000000000000001001 ,s = 0,E為全0,這時候就按照E為全0時的讀取方式來還原V = (-1)^0 * 0.00000000000000000001001 * 2^-126;這就是一個趨向于0的很小的正數(shù),以%f的形式打印出來就是0.000000取小數(shù)點后六位。
接下來,*pa = 9.0,表示將pa指向的對象內容改為9.0,此時n的內容就為9.0了,而9.0是以浮點型的形式來表示的。
我們先將十進制轉換為二進制數(shù) ==>1001.0
s = 0,M = 1.001,E = 3
V = (-1)^0 * 1.001 * 2^3
存儲值 = E + 127 = 130---->10000010
最后以0 10000010 00100000000000000000000存入
當%d站在它的角度進行讀取時,認為01000001000100000000000000000000就是補碼,最高位符號位為0,表示正數(shù)補碼 = 原碼 ,我們再將它化為十進制就得到了最后的結果。
當%f讀取的時候就按照浮點型的讀取方式打印出來,結果就為9.000000
通過講解這個例子我們也再次驗證了整型數(shù)和浮點型數(shù)在內存中的存儲方式和讀取方式都不一樣。正確的方式就應該是整型數(shù)按照整型的方式進行存放和讀取,浮點數(shù)按照浮點型的方式進行存放和讀取。
關于浮點型數(shù)據(jù)的存儲就講完了,下面來看看我們在使用浮點型時常出現(xiàn)的錯誤。
四. 關于浮點型經(jīng)常出現(xiàn)的錯誤
我們先來看一個例子,大家認為這段程序有問題嗎?
#include<stdio.h>int main() {int num = 0;scanf("%d", &num);if (num > 600){printf("%d\n",1.5 * num - 650);}else{printf("%d\n", num * 2);}return 0; }既然我把這個題放在這個地方那么它肯定就是有問題的,問題出在哪里呢?
問題就出來打印第一個結果那里,1.5為double型C語言自動轉換不同類型的行為稱之為隱式類型轉換 ,轉換的基本原則是:低精度類型向高精度類型轉換,此時整個表達式的結果就轉化為double型了,我們知道double型占8個字節(jié),而%d是打印有符號十進制整數(shù)的,此時必然會發(fā)生截斷,截斷意味著數(shù)據(jù)有丟失,那么結果就一定會出現(xiàn)問題。
我們一起來看看當num > 600時打印出來的結果:
這是非常容易犯的錯誤,即使你是大佬我覺得稍不留神也會犯這樣的錯誤,所以我們平時一定要細心一點,遇到浮點型數(shù)據(jù)一定要想到用浮點型來接收或者將它強轉為整型。
下面我們繼續(xù)來看看例子,我知道這是一個很明顯的錯誤但我想讓大家猜猜它會一直打印出什么?
我們一起來看看下面的結果:
我們發(fā)現(xiàn)什么?在int型范圍內以%lf打印出來都為0,下面我來解釋一下:
假設我們的整數(shù)為int型的最大正整數(shù)0xFFFFFFFF,那么在以%lf打印時,我們轉化為double型此時變?yōu)榱?x00000000FFFFFFFF,那么你想想我們的符號位(S)占一個,11個階數(shù)(E), 52個尾數(shù)(M),那么你想想我的階數(shù)E是不是為全0呢?這里用十六進制是用來表示的,一個16進制位表示4個二進制位,既然為全0,那么你想想我們在浮點型數(shù)據(jù)在內存中的存儲那里是不是講到過E為全0的話,此時還原回來為一個很小的數(shù)V = (-1)^0 * 2 ^ -1022 * M;這就是為什么總是打印出0的原因了。如果是long long的話那么就有可能不是0哦,因為long long占8個字節(jié),完全能使E不為全0,感興趣的讀者下來可以試一試。
五. 浮點型數(shù)據(jù)與"零值"的比較
講完上一部分相信大家對浮點型數(shù)據(jù)有了一定的了解,接下來我們來看一個例子:
你發(fā)現(xiàn)了什么?我們想為什么會出現(xiàn)單精度和雙精度呢?
原因就是它根本不是一個準確的數(shù)字,浮點數(shù)在內存中存儲并不想我們想的那樣是完整存儲的,在十進制轉化成為二進制,是有可能有精度損失的。注意這里的損失,不是一味的減少了,還有可能增多。浮點數(shù)本身存儲的時候,在計算不盡的時候,會“四舍五入”或者其他策略,這里在我們我們講第一個話題浮點數(shù)如何轉為二進制的時候我們就知道有些數(shù)字可能是無限位數(shù)的。
那么接下來大家來看一個例子,大家覺得它會打印出什么呢?相信我絕對不會說大家壞話的🙈🙈
int main() {double x = 1.0;double y = 0.1;if ((x - 0.9) == y){printf("you see you one day day,only eat meal.\n");}else{printf("Amazing\n");}return 0; }要充分相信博主一定不會說大佬們的壞話的嘿嘿
此時打印出Amazing一點也不意外,因為在上面我們已經(jīng)了解到浮點型數(shù)據(jù)并不能表示一個完整的數(shù),所以它們也是不會相等的,再看下圖我們證明一下:
浮點數(shù)本身有精度損失,進而導致各種結果可能有細微的差別,而對于我們的計算機來說細微的差別也是不相等的。
結論:浮點數(shù)在進行比較的時候,絕對不能直接使用 == 來進行比較!!!
那么我們該如何將浮點數(shù)與“零值”進行比較呢?有倆種方法:
法一:
自己設置一個精度,假如該值在這個誤差精度范圍之內就認為兩者相等。
我們在平時做oj題的時候題目是不是也經(jīng)常要求你輸出幾位小數(shù)呢?這樣是為了確保此時輸出的是一個完整的數(shù)。但是之前我遇到過一個很惡心的題,那時候博主是一位炒雞炒雞大萌新,當時那道題就要自己設定一個精度再進行判斷🙈🙈 那時候的我根本不理解哈哈。
如下圖所示:
此時就能打印出我想跟各位大佬們說的話了🙈🙈
法二:
引用float.h頭文件,使用系統(tǒng)推薦。
DBL_EPSILON double 最小精度
FLT_EPSILON float 最小精度
我們單擊DBL_EPSILON轉到定義,在float.h頭文件中找到它。
XXX_EPSILON是最小誤差是:XXX_EPSILON+n不等于n的最小的正數(shù)。
EPSILON這個單詞翻譯過來是’ε’的意思,數(shù)學上,就是極小的正數(shù)。
下面我們就來使用系統(tǒng)推薦的精度進行打印結果,結果是可以的:
最后我們來講講float/double變量與“零值”的比較,通過以上的栗子以及結論,我們的float/double型變量與"零值"的比較,最終可以寫成這樣:
如下三種方法我都使用的是系統(tǒng)定義的精度,讀者也可以自行定義一個精度
if (fabs(x-0.0) < DBL_EPSILON) //寫法1
if (fabs(x) < DBL_EPSILON) //寫法2
if(x > -DBL_EPSILON && x < DBL_EPSILON) //寫法3
我們來看看相關例子,只要x的精度控制得當x是能等于0.0的:
這是我隨便給x初始化的一個精度,讀者也可以自行設定,有時候設置得當x與0.0相等,設置不得當x就與0.0不相等了。
那么最后還有個問題我們到底要不要寫成小于等于最小精度,注意我們之前一直都是這樣寫的,例如:fabs((x - 0.9) - y) < DBL_EPSILON,而在大部分資料上是寫成fabs((x - 0.9) - y) <= DBL_EPSILON這樣的形式,那我們該如何進行理解呢?
個人看法:XXX_EPSILON是最小誤差,是:XXX_EPSILON+n不等于n的最小的正數(shù)。XXX_EPSILON + n是不等于n的最小的正數(shù),有很多數(shù)字 +n 都可以不等于n,但是XXX_EPSILON是最小的,但是XXX_EPSILON依舊是引起不等的一員。換句話說:fabs(x) <= DBL_EPSILON(確認x是否是0的邏輯),如果 =,就說明x本身,已經(jīng)能夠引起其他和他±的數(shù)據(jù)本身的變化了,這個不符合0的概念,0加上任何數(shù)等于它本身,這里就有點前后矛盾了。寫成小于最小精度的范圍不局限與某種精度值,所以我建議大家以后還是寫成小于比較好,這種是更加準確的。
六. 個人對于浮點型的看法和做題經(jīng)驗
一:大家在平時做oj題的時候盡量使用double型雙精度,因為在曾經(jīng)我有過因為使用float卡題的現(xiàn)象,double型表示的精度更為準確。
二:平時我們見到的小數(shù)默認是double型,那么我們要使用float類型的話要在后面加上f,表示它為一個float類型的數(shù)據(jù)。
三:遇到計算什么平均數(shù)以及計算出小數(shù)把它存在一個浮點型變量里面一定要保證表達式中有浮點型數(shù)據(jù)的出現(xiàn),例如:博主經(jīng)常在前面乘以1.0,這樣就保證了表達式中一定有一個浮點型數(shù)據(jù)的出現(xiàn),而不至于向下取整得不到我們想要看到的結果。
我們可以看看下圖它打印出來的結果就是向下取整之后的小數(shù):
為了避免出現(xiàn)這種情況我們要保證浮點型數(shù)據(jù)的出現(xiàn),此時我們就乘以1.0,既不會改變原來的數(shù)據(jù)也保證了小數(shù)的出現(xiàn)。這個例子博主舉的不是很好,因為它是一個無限不循環(huán)小數(shù)了,大家下來可以嘗試一下其他的例子。
好了,以上就是今天要講的全部內容了。關于整型和浮點型數(shù)據(jù)在內存中的存儲以及常見的錯誤都給大家總結好了,大家可以好好看看這倆篇文章哦,也是耗費了博主不少的時間進行大量測試和總結得出來的結論,希望看完之后會對你們有收獲哦 同時也別忘了給博主點點贊哦嘿嘿🙈🙈 祝大家程序員節(jié)日快樂,比較巧的是從今天開始俺也是一名19 year olds的老小子啦,希望大家在變強的同時也要主要身體哦😀😀
總結
以上是生活随笔為你收集整理的浮点型数据在内存中是如何存储的的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Oracle的Replace函数与tra
- 下一篇: 文本格式化