️作者:@malloc不出对象
⛺专栏:《初识C语言》
个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐

目录

  • 前言
    • 一. 浮点型数据如何存储的规则(理论部分)
    • 二. 浮点数怎么转化为二进制
    • 三. 浮点型数据在内存中的存储
    • 四. 关于浮点型经常出现的错误
    • 五. 浮点型数据与”零值”的比较
    • 六. 个人对于浮点型的看法和做题经验

前言

本篇文章博主将给大家讲讲浮点型数据在内存中的存储,关于浮点型这个地方其实有很多细节是需要我们去注意的,也是我们经常容易出现错误的地方,在这篇文章中我都会给大家总结出来,让你对浮点型不再感到疑惑。

一. 浮点型数据如何存储的规则(理论部分)

根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式:

V = (-1) ^ S * M + 2^E。
S表示符号位,当s = 0,V为正数,当s = 1,V为负数。
阶码部分(E)(指数部分),2^E(表示指数位)
M表示有效数字,大于等于1,小于2, 浮点数的精度就是由尾数来决定的。

IEEE 754规定:对于32位的浮点数,最高位是符号位s,接着的8位是指数E,剩下的23位为有效数字M,如下图所示:

对于64位的浮点数,最高位是符号位S,接着的11位是指数E,剩下的52位为有效数字M,如下图所示:

IEEE 754对有效数字M和指数E,还有一些特别规定。

前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。
IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。

至于指数E,情况就比较复杂。
首先,E为一个无符号整数(unsigned int)这意味着,如果E8位,它的取值范围为0\~255;如果E11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,但E为无符号整数不存在符号位,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,使其变为一个正整数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^-1的E是-1,所以保存成32位浮点数时,必须保存成-1+127=126,即01111110注意这个地方为存储值而非真实值.
好了,讲了这么多什么约定,那接下来我们来看看浮点数是怎么化为二进制的。

二. 浮点数怎么转化为二进制

首先我们来个简单的例子:

把十进制小数5.25化为二进制小数,我们应该怎么操作?
我们分为以下几步:
1. 以小数点为界进行拆分;
2. 整数部分转为二进制相信大家肯定没问题
3. 小数部分采用的是”乘2取整法”,当乘2之后小数部分得到0就停止计算

4. 合并结果:整数部分 + 小数部分,最终得到二进制结果为101.01.

我们来进行检验一下,发现确实如我们计算的这样:

以上就是浮点数化为二进制的步骤了,下面我们来看看更复杂一点的例子:
把十进制3.14化为二进制:

这里我就不带大家计算下去了,大家可以看下下图:

三. 浮点型数据在内存中的存储

我们先来看这个例子,大家想想浮点型数据是如何存进去的?跟整型数据比有什么区别?

#includeint 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

我们来检测一下是否如上述所计算得出的答案:

我们发现确实是如上图所计算得出的答案,在VS下采用的是小端模式因此是倒着存进去的。
为什么说E的情况比较复杂,其实存的时候E分为三种情况:

E不全为0或不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(or 1023),得到真实值,再将有效数字M前加上第一位的1。比如:0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码为-1+127=126,表示为01111110,而尾数1.0去掉整数部分为0,补齐02300000000000000000000000,则其二进制表示形式为0 01111110 00000000000000000000000,这是属于正常情况。

E为全0
这时,浮点数的指数E等于1~127(or 1~1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。

如果E为全0,此时的E为存储值,那么想一下E的真实值是不是为-127呢? 如果我们还原回去V = (-1)^ s * 1.xxxxx * 2^ -127,那么这是不是一个非常小的数呢,它趋向于±0 。此时就有了上面的规定。

E为全1
这时,如果有效数字M全为1,表示±无穷大(正负取决于符号位s)。

如果E为全1,此时存储值为255,减去127即为E的真实值,E = 128,这时如果我们把它还原回去V = (-1)^s * 1.xxxxxx * 2^128/1024;那么此时将为一个正负无穷大的数字。

好了,关于浮点型的存储规则就讲到这儿了,下面我们来看一道例题:

这里就来详细剖析一下这个结果,大家跟着我来分析一遍:

首先我们为n开辟4个字节大小的空间,&n代表的类型为int* 型,我们进行强制类型转换使它变为float*型。
int n = 9,表示n以整型的形式进行存储,当%d站在它的角度进行读取时,认为它是以整形的存储方式存放在内存中的,就按照整型的读取方式打印出来,n为正数它的原反补码一致,所以补码就为00000000 00000000 00000000 00001001,转化为十进制结果就为9
当站在%f它的角度进行读取时,认为它是以浮点型的存储方式存放在内存中的,就按照浮点型的读取方式打印。
00000000 00000000 00000000 00001001–>0 00000000 00000000000000000001001s = 0,E为全0,这时候就按照E为全0时的读取方式来还原V = (-1)^0 * 0.00000000000000000001001 * 2^-126;这就是一个趋向于0的很小的正数,以%f的形式打印出来就是0.000000取小数点后六位。
接下来,*pa = 9.0,表示将pa指向的对象内容改为9.0,此时n的内容就为9.0了,而9.0是以浮点型的形式来表示的。
我们先将十进制转换为二进制数 ==>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,表示正数补码 = 原码 ,我们再将它化为十进制就得到了最后的结果。


%f读取的时候就按照浮点型的读取方式打印出来,结果就为9.000000

通过讲解这个例子我们也再次验证了整型数和浮点型数在内存中的存储方式和读取方式都不一样。正确的方式就应该是整型数按照整型的方式进行存放和读取,浮点数按照浮点型的方式进行存放和读取。

关于浮点型数据的存储就讲完了,下面来看看我们在使用浮点型时常出现的错误。

四. 关于浮点型经常出现的错误

我们先来看一个例子,大家认为这段程序有问题吗?

#includeint 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.5double型C语言自动转换不同类型的行为称之为隐式类型转换 ,转换的基本原则是:低精度类型向高精度类型转换,此时整个表达式的结果就转化为double型了,我们知道double型占8个字节,而%d是打印有符号十进制整数的,此时必然会发生截断,截断意味着数据有丢失,那么结果就一定会出现问题。
我们一起来看看当num > 600时打印出来的结果:

这是非常容易犯的错误,即使你是大佬我觉得稍不留神也会犯这样的错误,所以我们平时一定要细心一点,遇到浮点型数据一定要想到用浮点型来接收或者将它强转为整型。

下面我们继续来看看例子,我知道这是一个很明显的错误但我想让大家猜猜它会一直打印出什么?

我们一起来看看下面的结果:

我们发现什么?在int型范围内以%lf打印出来都为0,下面我来解释一下:

假设我们的整数为int型的最大正整数0xFFFFFFFF,那么在以%lf打印时,我们转化为double型此时变为了0x00000000FFFFFFFF,那么你想想我们的符号位(S)占一个,11个阶数(E), 52个尾数(M),那么你想想我的阶数E是不是为全0呢?这里用十六进制是用来表示的,一个16进制位表示4个二进制位,既然为全0,那么你想想我们在浮点型数据在内存中的存储那里是不是讲到过E为全0的话,此时还原回来为一个很小的数V = (-1)^0 * 2 ^ -1022 * M;这就是为什么总是打印出0的原因了。如果是long long的话那么就有可能不是0哦,因为long long8个字节,完全能使E不为全0,感兴趣的读者下来可以试一试。

五. 浮点型数据与”零值”的比较

讲完上一部分相信大家对浮点型数据有了一定的了解,接下来我们来看一个例子:

你发现了什么?我们想为什么会出现单精度和双精度呢?

原因就是它根本不是一个准确的数字,浮点数在内存中存储并不想我们想的那样是完整存储的,在十进制转化成为二进制,是有可能有精度损失的。注意这里的损失,不是一味的减少了,还有可能增多。浮点数本身存储的时候,在计算不尽的时候,会“四舍五入”或者其他策略,这里在我们我们讲第一个话题浮点数如何转为二进制的时候我们就知道有些数字可能是无限位数的。

那么接下来大家来看一个例子,大家觉得它会打印出什么呢?相信我绝对不会说大家坏话的

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一点也不意外,因为在上面我们已经了解到浮点型数据并不能表示一个完整的数,所以它们也是不会相等的,再看下图我们证明一下:

浮点数本身有精度损失,进而导致各种结果可能有细微的差别,而对于我们的计算机来说细微的差别也是不相等的。
结论:浮点数在进行比较的时候,绝对不能直接使用 == 来进行比较!!!

那么我们该如何将浮点数与“零值”进行比较呢?有俩种方法:

法一:
自己设置一个精度,假如该值在这个误差精度范围之内就认为两者相等。

我们在平时做oj题的时候题目是不是也经常要求你输出几位小数呢?这样是为了确保此时输出的是一个完整的数。但是之前我遇到过一个很恶心的题,那时候博主是一位炒鸡炒鸡大萌新,当时那道题就要自己设定一个精度再进行判断 那时候的我根本不理解哈哈。
如下图所示:

#include #include #define EPSILON 0.0000000000001 //自定义的精度int main(){double x = 1.0;double y = 0.1;//if (((x - 0.9) - y) > - EPSILON && ((x - 0.9) - y) < EPSILON)if (fabs((x - 0.9) - y) < EPSILON)//简洁版,fabs用来求浮点数的绝对值,要引用math.h这个头文件{printf("各位大佬,请您教菜鸟技术\n");} else{printf("Amazing\n");} return 0;}

此时就能打印出我想跟各位大佬们说的话了

法二:
引用float.h头文件,使用系统推荐。
DBL_EPSILON double 最小精度
FLT_EPSILON float 最小精度

我们单击DBL_EPSILON转到定义,在float.h头文件中找到它。

XXX_EPSILON是最小误差是:XXX_EPSILON+n不等于n的最小的正数。
EPSILON这个单词翻译过来是’ε’的意思,数学上,就是极小的正数。

下面我们就来使用系统推荐的精度进行打印结果,结果是可以的:

最后我们来讲讲float/double变量与“零值”的比较,通过以上的栗子以及结论,我们的float/double型变量与”零值”的比较,最终可以写成这样:

如下三种方法我都使用的是系统定义的精度,读者也可以自行定义一个精度
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的最小的正数。XXX_EPSILON + n是不等于n的最小的正数,有很多数字 +n 都可以不等于n,但是XXX_EPSILON是最小的,但是XXX_EPSILON依旧是引起不等的一员。换句话说:fabs(x) <= DBL_EPSILON(确认x是否是0的逻辑),如果 =,就说明x本身,已经能够引起其他和他±的数据本身的变化了,这个不符合0的概念,0加上任何数等于它本身,这里就有点前后矛盾了。写成小于最小精度的范围不局限与某种精度值,所以我建议大家以后还是写成小于比较好,这种是更加准确的。

六. 个人对于浮点型的看法和做题经验

一:大家在平时做oj题的时候尽量使用double型双精度,因为在曾经我有过因为使用float卡题的现象,double型表示的精度更为准确。

二:平时我们见到的小数默认是double型,那么我们要使用float类型的话要在后面加上f,表示它为一个float类型的数据。

三:遇到计算什么平均数以及计算出小数把它存在一个浮点型变量里面一定要保证表达式中有浮点型数据的出现,例如:博主经常在前面乘以1.0,这样就保证了表达式中一定有一个浮点型数据的出现,而不至于向下取整得不到我们想要看到的结果。

我们可以看看下图它打印出来的结果就是向下取整之后的小数:

为了避免出现这种情况我们要保证浮点型数据的出现,此时我们就乘以1.0,既不会改变原来的数据也保证了小数的出现。这个例子博主举的不是很好,因为它是一个无限不循环小数了,大家下来可以尝试一下其他的例子。

好了,以上就是今天要讲的全部内容了。关于整型和浮点型数据在内存中的存储以及常见的错误都给大家总结好了,大家可以好好看看这俩篇文章哦,也是耗费了博主不少的时间进行大量测试和总结得出来的结论,希望看完之后会对你们有收获哦 同时也别忘了给博主点点赞哦嘿嘿 祝大家程序员节日快乐,比较巧的是从今天开始俺也是一名19 year olds的老小子啦,希望大家在变强的同时也要主要身体哦