浮点类型详解

本文深入探讨浮点数的基础知识,包括十进制与二进制、十六进制之间的转换,以及浮点数在计算机中的存储格式、规范化、舍入处理和溢出操作。通过实例详细解释了浮点数计算过程,帮助读者理解浮点数在编程中的工作原理。

目录

前言

基础知识

十进制浮点数与二进制浮点数的转换

十六进制数浮点数

浮点数的存储

浮点数存储的格式

规范化

舍入处理

溢出操作

浮点数的计算 


 注:所有的[斜体]内容都是本人自己杜撰的概念,只代表个人理解,可能不能与科学规范的学术概念相吻合

             所有的{   }包含的是一个格式

             所有的"   "包含的是一个对象实体

前言

从一个简单的程序里发现了点蹊跷

float a = 2.0e20+1.0;
float b = a - 2.0e20;
printf("%a",b);
┌──(ivaldi㉿kali)-[~/Desktop/C/float]
└─$ cd "/home/ivaldi/Desktop/C/float/" && gcc main.c -o main && "/home/ivaldi/Desktop/C/float/"main
0x1.d29cfp+41

基础知识

十进制浮点数与二进制浮点数的转换

首先抛开计算机存储的限制,我们来看看如何把一个[十进制浮点数]转化为一个[二进制浮点数]:

0.分离[十进制浮点数]为[符号位]和[不带符号位的十进制浮点数],这个[符号位]与[二进制浮点数]中所包含的[符号位]相同

1.分离[不带符号位的十进制浮点数]为[不带符号位的十进制浮点数的整数部分]和[不带符号位的十进制浮点数的小数部分]

2.[不带符号位的十进制数的整数部分]采用"除以2取余数,倒序相加"的方法,转化为[不带符号位的二进制数的整数部分]

3.[不带符号位的十进制数的小数部分]采用"乘以2取整数,顺序相加"的方法,转化为[不带符号位的二进制数的小数部分]

4.按照{[符号位]+[不带符号位的二进制数的整数部分]+[小数点] "."+[不带符号位的二进制数的小数部分]}的格式拼接在一起就变成了[二进制浮点数]

下面是一个简单的例子:

[十进制浮点数] "-4.8125" 转化为 [二进制浮点数]

(0)[十进制浮点数] "-4.8125" 分成 [符号位] "-" 和 [不带符号位的十进制浮点数] "4.8125"

(1)[不带符号位的十进制浮点数] "4.8125" 分成 [不带符号位的十进制浮点数的整数部分] "4和[不带符号位的十进制浮点数的小数部分] "0.8125"

(2)[不带符号位的十进制浮点数的整数部分] "4" 转化为[不带符号位的二进制数的整数部分] "100"

        4 除以 2 = 2 ...... 0

                         2 除以 2 = 1......0

                                          1 除以 2 = 0......1       

        得到商为0就没必要继续除了,如果继续除,得到的商和余数都是0,就进入无限循环了

        把依次得到的余数,按照倒序,连接在一起,变成"100",这就是[不带符号位的二进制数的整数部分]的结果

(3)[不带符号位的十进制浮点数的小数部分] "0.8125" 转化为[不带符号位的二进制数的小数部分] "1101"

        0.8125 乘以 2 = 0.625 + 1

                                  0.625 乘以 2 = 0.25 + 1

                                                          0.25 乘以 2 = 0.5 + 0

                                                                                0.5 乘以 2 = 0.0 + 1

        得到小数部分为0就没必要继续乘了,如果继续乘,得到的积都是0,就进入无限循环了

        把依次得到的整数部分,按照顺序,连接在一起,变成"1101",这就是[不带符号位的二进制数的小数部分]的结果

(4)按照{[符号位] "-" +[不带符号位的二进制数的整数部分] "100"+[小数点] "."+[不带符号位的二进制数的小数部分] "1101"}的格式,拼接在一起,就变成了[二进制浮点数]"-100.1101",当然,这也可以表示为"-1.001101*2^2"

十六进制数浮点数

[十六进制数浮点数]是[二进制浮点数]的另一种表达形式,差别在于:

  • ANSI-C中支持[十进制浮点数]格式的浮点型常量的书写,但是不支持[二进制浮点数]格式的浮点型常量的书写
  • [二进制浮点数]只是计算机硬件层面的一种表现方式和原理体现
  • C99标准中新增加了对[十六进制数浮点数]格式的浮点型常量的书写的支持
  • [十六进制数浮点数]本质还是[二进制浮点数],只是[二进制浮点数]的另一种表达形式

将一个[二进制浮点数]转化为[十六进制数浮点数]的步骤:

0.将[二进制浮点数]拆解成[符号位]+[不带符号位的二进制浮点数]

1.将[不带符号位的二进制浮点数]拆解成[不带符号位的二进制浮点数的整数部分]+[小数点]+[不带符号位的二进制浮点数的小数部分]

2.[不带符号位的二进制浮点数的整数部分]从右往左,每4个一组,不足补0,转化为十六进制数,变成[不带符号位的十六进制数浮点数的整数部分]

3.[不带符号位的二进制浮点数的小数部分]从左往右,每4个一组,不足补0,转化为十六进制数,变成[不带符号位的十六进制数浮点数的小数部分]

4.按照{[十六进制前缀]+[符号位] +[不带符号位的十六进制数浮点数的整数部分]+[小数点]+[不带符号位的十六进制数浮点数的小数部分]}的格式,合成[十六进制数浮点数]

还是上面那个简单的例子:

"-100.1101"  ->  "-"+"100"+"."+"1101"  ->  "-"+"0100"+"."+"1101" ->""0x"+"-"+"8"+"."+"d"  -> "0x-8.d"

浮点数的存储

浮点数存储的格式

再来看看浮点数在计算机中是如何进行存储的:

对于32bits的float型浮点数,其各个位存储的数据如下

313029282726252423222120191817161514131211109876543210
符号位阶数位                尾数位

对于64bits的double型浮点数,其各个位存储的数据如下

6362616059585756555453525150494847464544434241403938373635343332313029282726252423222120191817161514131211109876543210
符号位阶数位尾数位

规范化

为了方便浮点型数值在计算机中的存储,一个浮点数值在计算机中只能有一种表达形式,IEEE标准规定,一个[不带符号位的二进制数]需要规范化为{\pm1.xxxx * 2^n}的形式,其中,n为一个[有符号的十进制数]

规范化操作有两种方式:

        左规:这是默认的规范化操作方式,每次将尾数位左移1位,并将阶数位-1

        右规:只有当浮点数计算出现进位时,才会右规,将尾数位右移1位,并将阶数位+1.一次规范化中至多发生一次右规

比如:[十进制浮点数]"+2.0e+20"转化为[二进制浮点数]为"+10101101011110001110101111000101101011000110001000000000000000000000.0"

                                          其[规范化的二进制浮点数]为"+1.01011010111100011101011110001011010110001100010000000000000000000000*2^6^7"

                                       其[规范化的十六进制数浮点数]为"0x1.5af1d78b58c4p67" 

这里有几个小知识点要进行补充和说明:

0.[十进制浮点数]使用e/E来表示10的次方,[二进制浮点数]使用p/P来表示2的次方

1.表示次方的n都是一个[有符号的十进制数]

2.前面说了,[十六进制数浮点数]本质上还是[二进制浮点数],p/P后面接的n次方,指的是在二进制下小数点的位移,而不是十六进制下

3.可以观察到,[规范化的不带不带符号的二进制浮点数]除了0.0,其整数部分永远为1,所以计算机存储时,会忽略这个整数部分,只将小数部分存入尾数位

4.理论上,经过规范化处理的浮点数,其小数部分就是实际存储在计算机浮点数的尾数部分的二进制码,但是现实是,因为存储尾数的空间有限,有时因为尾数位过长,而不能全部存进去,此时就会进行舍入处理.

舍入处理

浮点数字的默认舍入方式是就近舍入,相当于我们常说的"四舍五入",这里我们将可以存入的位称为[有效位],无法存入的位称为[多余位]:

0.若[多余位]首位为0,则直接省去[多余位]   

1.若[多余位]首位为1,且其余位不全为0,则[有效位]加1

2.若[多余位]等于1000....0000,且[有效位]末位为1,则[有效位]加1

3.若[多余位]等于1000....0000,且[有效位]末位为1,则直接省去[多余位]

这里还是拿上面的2.0E20举例子,理论上需要存入尾数位的二进制码为"0101101011110001110101111000101101011000110001"

对于64bits的double型内存空间,尾数位为51bits,因此会将原二进制码截断成[有效位]"010110101111000111010111100010110101100011000100000"和[多余位]"0000000000000000",[多余位]会被直接舍去.这时,尾数位存储的二进制码为:010110101111000111010111100010110101100011000100000

而对于32bits的float型内存空间,尾数位为23bits,因此会将原二进制码截断成[有效位]"01011010111100011101011"和[多余位]"11000101101011000110001000000000000000000000",[有效位]+1变成[有效位]"01011010111100011101100",这时,尾数位存储的二进制码为:01011010111100011101100

溢出操作

[规范化的二进制浮点数]的指数,理论上是可以直接以[有符号数]的补码形式直接存的,补码也不会产生歧义,但是为了方便浮点数之间进行比较,规定采用阶码(补码+偏码)的方式来存储指数

  • 对于32bits的float型浮点数,其阶数位为8位,规定其偏码为2^7-1 = 127,即0B 0111 1111
  • 对于64bits的double型浮点数,其阶数位为11位,规定其偏码为2^1^0-1 = 1023,即0B 011 1111 11111

2.0E20对应的[规范化的二进制浮点数]的指数为67,其补码为0B 0100 0011,当然也可以是0B 000 0100 0011

  • 对于32bits的float型浮点数,67对应阶码为 0B 0111 1111+ 0B 0100 0011 = 0B 1100 0010 
  • 对于64bits的double型浮点数,67对应阶码为 0B 011 1111 1111+ 0B 000 0100 0011 = 0B 100 0100 0010 

对于每种类型,操作系统都保留了两个阶码000...000和111...111,用来判断溢出操作,我们先看看这两个阶码原本对应什么十进制数

  • 对于32bits的float型浮点数,11111111 - 01111111 = 1000 0000,对应十进制-128,  0000 0000 - 0111 1111 = 1000 0001,对应十进制-127
  • 对于64bits的double型浮点数,111 1111 1111 - 011 1111 1111 = 100 0000 0000,对应十进制-1024,  000 0000 0000 - 011 1111 1111 = 100 0000 0001,对应十进制-1023

也就是说,实际上,指数并不能取到最低的两个,这么设计的理由是希望一个类型能表示的数字的范围尽可能的大

系统保留的两个阶码被用来进行溢出的判断,浮点数的溢出只根据阶码来判断,这里只拿float来讲讲,float的阶码范围为0000 0001到 1111 1110

  • 若一个有效的浮点数,其阶码为0000 0001,现在要把这个数除以2,我们从[规范化的二进制浮点数]格式可以看出,这个数的尾数部分不会发生改变,但是阶码需要减1,就变成了0000 0000,操作系统就会判定这个数字出现了溢出,因为是超过了最小能表示的值,所以叫做下溢
  • 若一个有效的浮点数,其阶码为1111 1110,现在要把这个数乘以2,我们从[规范化的二进制浮点数]格式可以看出,这个数的尾数部分不会发生改变,但是阶码需要加1,就变成了1111 1111,操作系统就会判定这个数字出现了溢出,因为是超过了最大能表示的值,所以叫做上溢
  • 同理的,只要一个[规范化的二进制浮点数],其指数只要超过了阶码所能表示的范围,就是溢出.操作系统对于溢出采取了一些规定如下:
  1. 若浮点数的符号为正,且溢出方式为上溢,称为正上溢,记为+\infty,表示为inf
  2. 若浮点数的符号为负,且溢出方式为上溢,称为负上溢,记为-\infty,表示为-inf
  3. 若浮点数的符号为正,且溢出方式为下溢,称为正下溢,记为0
  4. 若浮点数的符号为正,且溢出方式为下溢,称为负下溢,记为0

浮点数的计算 

现在再来回顾一开始这个非常简单的程序

1.我们先把2.0E20和1.0转化为 [规范化的二进制浮点数]:

        2.0E20 => 1.01011010111100011101011110001011010110001100010000000000000000000000*2^6^7

         1.0 => 1.0*2^0

2.舍入处理,并存储为内存中的临时数据

        2.0E2       

6362616059585756555453525150494847464544434241403938373635343332313029282726252423222120191817161514131211109876543210
0100010000100101101011110001110101111000101101011000110001000000
符号位阶数位尾数位

                1.0

6362616059585756555453525150494847464544434241403938373635343332313029282726252423222120191817161514131211109876543210
0011111111110000000000000000000000000000000000000000000000000000
符号位阶数位尾数位


3.对阶

浮点数进行计算前,先要把让两个浮点数的阶数相同,且为向阶数大的一方对齐,操作为:连续右移阶数较小者的尾数位,每位移一次,阶数加1,直至相等

这里需要注意的是1.0原来含有隐藏位1,在右移过程中,该隐藏位也会纳入位移的范围,即第一次右移后,1.0对应的尾数位变为 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000,隐藏位变为0,对阶完成的时候,1.0的阶数变成10001000010,而由于右移次数过多,尾数为依然全为0,仿佛没有发生变化

这里另外还有提一句,虽然不是每种编译器都支持扩展双精度类型(long double),扩展双精度类型不存在1位隐藏位,它的尾数位是多少,这个数就是多少,说起来还是挺喜欢这种"直白"的类型的

4.尾数计算,得到新的尾数 0101 1010 1111 0001 1101 0111 1000 1011 0101 1000 1100 0100 0000,将这个尾数,阶数位和符号位存入变量a对应的内存,这时,由于a为float类型,而浮点常量默认为double类型,尾数位要进行截断,阶数位要进行转化,因此要进行舍入处理和溢出判断

根据之前的内容,尾数位舍入为0101 1010 1111 0001 1101 100,阶数位转化为:1100 0010 ,即变量a所在内存空间中的每个位为:

313029282726252423222120191817161514131211109876543210
01100001001011010111100011101100
符号位阶数位                尾数位

5.第二条语句中的详细操作后面就不赘述太多,简要说明一下:

        将变量a对应内存的float类型的浮点数读出,重新分配临时空间,将其转化为一个double类型的常量,包括阶码的转化,尾数位补0

        将浮点常量2.0E20,存储为一个临时的double类型常量

        对阶,由于阶码本来就相等,就不进行操作

        由于是减法操作,2.0E20对应内存的[有效位](隐藏位+尾数位)需要整体作为原码,变成补码,即由010101101011110001110101111000101101011000110001000000变成101010010100001110001010000111010010100111001111000000     

        尾数加减(其实我更愿意称呼为有效位加减)

           01. 0101 1010 1111 0001 1101 1000 0000 0000 0000 0000 0000 0000 0000   

        + 10. 1010 0101 0000 1110 0010 1000 0111 0100 1010 0111 0011 1100 0000     

---------------------------------------------------------------------------------------------------------------------

           00. 0000 0000 0000 0000 0000 0000 0111 0100 1010 0111 0011 1100 0000                      

        规范化,有效位右移26位,有效位变成01.11 0100 1010 0111 0011 1100 0000...(后面的0懒得写了),阶码-26

        判断是否溢出,这里粗略看一下是没有溢出的,详细的也可以自己算算

        存储,将上述计算出来的double类型结果,储存在一个float类型中,最终存储在变量b中的各个位为:0 1010 1000 11 0100 1010 0111 0011 1100 0 

313029282726252423222120191817161514131211109876543210
01010100011010010100111001111000
符号位阶数位                尾数位

最后,我们也不难发现,这个结果正是我们上面得到的结果,至于阶码就自己算算吧

1101 0010 1001 1100 1111 000 

------------------------------------------

      d       2       9       c        f     0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值