小朋友学C语言(43):浮点数的深入分析

2019-03-06 13:10:25 浏览数 (1)

IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number)),一些特殊数值(无穷∞与非数值NaN),以及这些数值的“浮点数运算符”。 IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。只有32位模式有强制要求,其他都是选择性的。大部分编程语言都有提供IEEE浮点数格式与算术,但有些将其列为非必需的。例如,IEEE 754问世之前就有的C语言,现在有包括IEEE算术,但不算作强制要求 C语言的float通常是指IEEE单精确度,而double是指双精确度。

一、浮点数的表示方法

一个浮点数 (Value) 的表示其实可以这样表示: Value = sign * exponent * fraction。 也就是浮点数的实际值,等于符号位(sign bit)乘以指数偏移值(exponent bias)再乘以分数值(fraction)。分数值也称尾数值。

二、浮点数在内存中的存储方式

方式

位数

标准

模式

float

32bit

IEEE R32.24

第31位(最高位)存符号,第23-30位存指数部分,共8位,第0-22位位存尾数部分,共23位

double

64bit

IEEE R64.53

第63位(最高位)存符号,第52-62位位存指数部分,共11位,第0-51位存尾数部分,共52位

三、指数偏差

指数偏差(表示法中的指数为实际指数减掉某个值)为 ,其中的e为存储指数的比特的长度。减掉一个值因为指数必须是有号数才能表达很大或很小的数值,但是有号数通常的表示法——补码(two's complement),将会使比较变得困难。这是因为补码的大小很难直接看出来。 为了解决这个问题,指数在存储之前需要做偏差修正,将它的值调整到一个无符号数的范围内以便进行比较。此外,指数采用这种方法表示的优点还在于使得浮点数的正规形式和非正规形式之间有了一个平滑的转变。

四、指数偏移值

指数偏移值(exponent bias),是指浮点数表示法中的指数域的编码值为指数的实际值加上某个固定的值,IEEE 754标准规定该固定值为2e-1 - 1,其中的e为存储指数的比特的长度。 以单精度浮点数为例,它的指数域是8个比特,固定偏移值是28-1 – 1 = 127。此为有号数的表示方式,单精度浮点数的指数部分实际取值是从-128到127。例如指数实际值为1710,在单精度浮点数中的指数域编码值为1710 12710 = 14410采用指数的实际值加上固定的偏移值的办法表示浮点数的指数,好处是可以用长度为e个比特的无符号整数来表示所有的指数取值,这使得两个浮点数的指数大小的比较更为容易,实际上可以按照字典序比较两个浮点表示的大小。 这种移码表示的指数部分,中文称作阶码。

下面举一些例子来加深对移码的理解:

例1

如果我们要表示 0,则有0 127 = 127,用二进制表示即为 0000 0000 0111 1111= 0111 1111

例2

如果我们要表示1,则有1 127 = 128,用二进制表示即为 0000 0001 0111 1111 = 1000 0000

例3

如果我们要表示2,则有2 127 = 129,用二进制表示即为 0000 0010 0111 1111 = 1000 0001

例4

如果我们要表示128,则有128 127 = 255,用二进制表示即为 1000 0000 0111 1111 = 1111 1111 这个128是我们能够使用 8位二进制移位存储算法表示的最大的正数了,再大就溢出了。

我们再来看看负数:

例5

如果我们要表示-1,则有(-1) 127 = 127 – 1 = 126,用二进制表示即为 0111 1111 – 0000 0001 = 0111 1111

例6

如果我们要表示-2,则有(-2) 127 = 127 – 2 = 125,用二进制表示即为 0111 1111 – 0000 0010 = 0111 1101

例7

如果我们要表示-127,则有(-127) 127 = 127 – 127 = 0,用二进制表示即为 0111 1111 – 0111 1111 = 0000 0000 这个 -127是我们能够使用 8位二进制采用移位存储所能表示的最小的负数了,再小就会溢出。

由上面的分析我们可以得出规律,采用移位存储技术,我们可以使用 8位二进制来表示从 -127~128 共计:27个负数 零(0) 128个正数=256个数

例8:求十进制数8.25在内存中的储存方式

分析: 8.25用二进制形式表示为1000.01,表示成二进制的指数形式为1.00001 * 23,用科学计数法则表示为1.00001 * 2E3。 因为是正数,符号位即最高位为0; 指数位为3 127(移位存储) = 130,二进制形式是10000010; 尾数部分00001 = 0000100 00000000 00000000(23位)。 所以8.25在内存中储存为:0 10000010 00001000000000000000000

例9:二进制1 10000010 00001000000000000000000是一个单精度浮点数,对应的十进制数是多少?

分析: 最高位为1,表示负数; 指数位为10000010(2) = 130(10),130 – 127 = 3; 尾数为00001000000000000000000,换成十进制为1 1/32。注意这里的1不要忘了加。 所以表示的数为-(1 1/32) * 23 = -8.25

例10:求十进制数-0.125在内存中的存储方式

分析: -0.125用二进制表示为:-0.001,表示成二进制的指数形式为-1.0 * 2-3,用科学计数法则表示为-1.0 * 2E-3。 因为是负数,符号位即最高位为1; 指数位为-3 127 = 124,二进制形式是01111100(8位); 尾数部分为0000000 00000000 00000000(23位)。 所以-0.125在内存中储存为1 01111100 00000000000000000000000。

例11:二进制0 01111100 00000000000000000000000表示一个单精度浮点数,求对应的十进制数

符号位为0,表示这是一个正数; 011111002= 12410,十进制为124 – 127 = -3; 尾数00000000000000000000000表示1,注意这里的1不要忘了加。 所以十进制为1.0 * 2-3 = 0.125

例12:求十进制数120.5在内存中的存储方式

分析: 120.5用二进制形式表示为:1111000.1,表示成二进制的指数形式为1.1110001 * 26,用科学计数法则表示为1.1110001 * 2E6。 因为是正数,符号位即最高位为0; 指数位为6 127(移位存储) = 133,二进制形式是10000101; 尾数部分1110001 = 1110001 00000000 00000000(23位)。 所以120.25在内存中储存为:0 10000101 11100010000000000000000

例13:已知二进制01000001001000100000000000000000表示的是一个单精度浮点数,求它的十进制表示形式

分析: 最高位为0,所以这是个正数; 指数位为10000010,即十进制为130。所以指数为130 – 127 = 3。 尾数位为01000100000000000000000,换算成十进制为1 1/4 1/64。注意这里的1不要忘了加。 所以对应的十制数为23 * (1 1/4 1/64) = 8 2 0.125 = 10.125

五、规约形式的浮点数

如果浮点数中指数部分的编码值在1 <= exponent <= 2e - 2之间,且在科学表示法的表示方式下,尾数部分最高有效位(即整数字)是1,那么这个浮点数将被称为规约形式的浮点数。“规约”是指用唯一确定的浮点形式去表示一个值。 单精度(32-bit)的规约形式浮点数在指数偏移值的值域为00000001到11111110,尾数部分则是000……000(共23个0)到111……111(共23个1)。 双精度 (64-bit)的规约形式浮点数在指数偏移值的值域为00000000001到11111111110,尾数部分则是000……000(共52个0)到111……111(共52个1)。

例14:求规约数0 00000001 00000000000000000000000所表示的十进制数

分析: 第一个0表示正数; 指数位为1 – 127 = -126; 尾数位为1(这个1是隐藏的,别忘了加上)。 所以对应的十进制为2-126 ,这是规约数所能表示的最小的正数。 同理规约数1 00000001 00000000000000000000000表示的是最大的负数为-2-126。

例15:求规约数0 00000001 11111111111111111111111所表示的十进制数

分析: 尾数部分为(1 1/2 1/4 … 1/23),这个数略小于2但很接近于2。 所以对应的十进制接近但略小于2 * 2-126。

六、非规约形式的浮点数

如果浮点数的指数部分的编码值是0,分数部分非零,那么这个浮点数将被称为非规约形式的浮点数。一般是某个数字相当接近零时才会使用非规约型式来表示。IEEE754标准规定:非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值小1。 例如,最小的规约形式的单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规约的单精度浮点数的指数域编码值为0,对应的指数实际值也是-126而不是-127。 实际上非规约形式的浮点数仍然是有效可以使用的,只是它们的绝对值已经小于所有的规约浮点数的绝对值;即所有的非规约浮点数比规约浮点数更接近0。规约浮点数的尾数大于等于110且小于210,而非规约浮点数的尾数大于010且小于110。

例16:求非规约数0 00000000 00000000000000000000001所表示的十进制

分析: 因为是非规约数,所以指数位是-126,而不是0 – 127 = -127; 非规约数的尾数部分没有隐含的1,所以尾数部分为2-23; 所以对应的十进制为2-23 * 2-126 = 2-149,这是非规约数所能表示的最小的正数。 同理非规约数所能表示的最大负数为0 00000000 00000000000000000000001= -2-149。

例17:求非规约数0 00000000 11111111111111111111111所表示的十进制

分析: 因为是非规约数,所以指数位是-126,而不是0 – 127 = -127; 非规约数的尾数部分没有隐含的1,所以尾数部分为1/2 1/4 … 1/23 ≈ 1(但略小于1); 所以对应的十进制数略小于2-126。 这也是最大的正非规约数,接近但略小于最小的规约数2-126。 同理最大的负非规约数,接近但略大于最大的规约数-2-126。

七、浮点数的三个特殊值

这里有三个非规约浮点数的特殊值必须指出(以单精度为例):

例18:如果指数是0并且尾数的小数部分是0,则这个数是±0

代码语言:javascript复制
0 00000000 00000000000000000000000表示0
1 00000000 00000000000000000000000表示-0

例19:如果指数为2e – 1并且尾数的小数部分是0,这个数是±∞

对于float类型,因为指数位有8位,则e = 8,

代码语言:javascript复制
2<sup>e</sup> – 1 = 2<sup>8</sup> – 1 = 255 = 11111111<sub>2</sup>,则有
0 11111111 00000000000000000000000 = ∞
0 11111111 00000000000000000000000 = -∞

对于double类型,因为指数位有11位,则e = 11, 2e – 1 = 211 – 1 = 111111111112,则有

代码语言:javascript复制
0 11111111111 0000000000000000000000000000000000000000000000000000 = ∞
0 11111111111 0000000000000000000000000000000000000000000000000000 = -∞

例20:如果指数为2e – 1并且的尾数的小数部分非0,这个数表示NaN,即不是一个数

对于float类型,有

代码语言:javascript复制
0 11111111 00000000000000000000001 = NaN
0 11111111 00000000000000000000010 = NaN
0 11111111 00000000000000000000011 = NaN
……(为NaN的数共有223 – 1个)

对于double类型,有

代码语言:javascript复制
0 11111111111 0000000000000000000000000000000000000000000000000001 = NaN
0 11111111111 0000000000000000000000000000000000000000000000000010 = NaN
0 11111111111 0000000000000000000000000000000000000000000000000011 = NaN
……(为NaN的数共有2<sup>52</sup> – 1个)

八、浮点数的一些规律

由例14~例20可以看出浮点数的一些规律: ① 浮点数有三种表现形式:规约形式、非规约形式、三个特殊值。

形式

指数

小数部分

0

0

0

非规约形式

0

非0

规约形式

1 ~ 2e - 2

任意

无穷

2e - 1

0

NaN

2e - 1

非0

② 规约浮点数的尾数大于等于1且小于2。

③ 非规约数尾数部分大于0且小于1。

④ 绝对值最大的非规约数略小于绝对值最小的规约数2-126。

⑤ 所有的非规约数比所有的规约数更接近于0。(与第③点是同一个意思)

⑥ float浮点数可表示的大小为:

±0 (特殊值) ±2-126 * 2-23 = 2-149 (这两个数是绝对值最小的非规约数) ±2-126 * 2-22 = 2-148±2-126 (2-22 2-23) …… ±2-126 * (2-1 2-2 … 2-22 2-23) = ±2-126 * (1 - 2-23) (这两个数是绝对值最大的非规约数) ±2-126 (这两个数是绝对值最小的规约数) ±2-126 * (1 2-23) ±2-126 * (1 2-22) ±2-126 * (1 2-22 2-23) ±2-126 * (1 2-21) …… ±2127 * (1 2-1 2-2 2-23) = ±2127 * (2 - 2-23) (这两个数是绝对值最大的规约数) ±∞ (特殊值)

⑦ double浮点数与上面float浮点数的表示形式类似,只是指数和尾数不一样而已。

九、为何使用非规约形式的浮点数

IEEE754-1985标准采用非规约浮点数,用来解决填补绝对值意义下最小规格数与零的距离。非规约浮点数源于70年代末IEEE浮点数标准化专业技术委员会酝酿浮点数二进制标准时,Intel公司对渐进式下溢出(gradual underflow)的力荐。而当时十分流行的DECVAX机的浮点数表示采用了突然式下溢出(abrupt underflow)。 如果没有渐进式下溢出,那么0与绝对值最小的浮点数之间的距离(gap)将大于相邻的小浮点数之间的距离。例如单精度浮点数的绝对值最小的规约浮点数是2-126,绝对值次小的规约浮点数为(1 2-23) * 2-126,这两个数之间的距离为(1 2-23) * 2-126 - 2-126 = 2-149。而2-126与0之间的距离为2-126,如果不采用渐进式下溢出,那么绝对值最小的规约浮点数与0的距离是相邻的小浮点数之间距离的223倍!可以说是非常突然的下溢出到0。 采用了渐进式下溢出后将不会出现这种情况。例如对于单精度浮点数,指数部分实际最小值是(-126),对应的尾数部分从000……000,000……001,000……010,000……011 一直到111……101,111……110,111……111,每两个相邻两小浮点数之间的距离(gap)都是2-126 * 2-23 = 2-149;而0与最近的浮点数(即最小的非规约数)之间的距离也是2-149。

十、单精度浮点数各种极值情况

类别

正负号

实际指数

有偏移指数

指数域

尾数域

数值

0

-127

0

0000 0000

000 0000 0000 0000 0000 0000

0.0

负零

1

-127

0

0000 0000

000 0000 0000 0000 0000 0000

−0.0

1

0

0

127

0111 1111

000 0000 0000 0000 0000 0000

1.0

-1

1

0

127

0111 1111

000 0000 0000 0000 0000 0000

−1.0

最小的非规约数

0或1

-126

0

0000 0000

000 0000 0000 0000 0000 0001

约等于±1.4E-45

中间大小的非规约数

0或1

-126

0

0000 0000

100 0000 0000 0000 0000 0000

约等于±5.88E-37

绝对值最大的非规约数

0或1

-126

0

0000 0000

111 1111 1111 1111 1111 1111

约等于±1.18E 38

绝对值最小的规约数

0或1

-126

1

0000 0001

000 0000 0000 0000 0000 0000

约等于±1.18E 38

绝对值最大的规约数

0或1

127

254

1111 1110

111 1111 1111 1111 1111 1111

约等于±3.4E 38

正无穷

0

128

255

1111 1111

000 0000 0000 0000 0000 0000

负无穷

1

128

255

1111 1111

000 0000 0000 0000 0000 0000

−∞

不是一个数

0或1

128

255

1111 1111

non zero

NaN

十一、浮点数的整数范围

由上面的讨论可知,刨除掉无穷值后, 最大的单精度浮点数为:0 11111110 11111111111111111111111, 用十进数表示则为2127 * (1 1/2 1/4 … 1/223) ≈ 2 * 2127 ≈ 2 * 1.7 * 1038 = 3.4 * 1038 = 3.4E 38。 最小的单精度浮点数为:1 11111110 11111111111111111111111,用十进制表示则为 -3.4E 38。 所以,单精度浮点数能表示的整数范围约为-3.4E 38 ~ 3.4E 38。 同理,双精度浮点数能表示的整数范围约-1.79E 308 ~ 1.79E 308。

十二、浮点数的精度

浮点数的精度指的是有效数字。有效数字是指在一个数中,从该数的第一个非零数字起,直到末尾数字止的数字称为有效数字。 比如0.618的有效数字有三个,分别是6,1,8。 12.345的有效数字有5个。 12.34500的有效数字有7个。 0.00123的有效数字有3个。

float和double的精度是由尾数的位数来决定的。浮点数在内存中是按科学计数法来存储的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响。 对于float而言,因为这个“1”的存在,虽然尾数只有23位,但实际上可用来表示尾数的有24位。精度问题就相当于变成这样的问题:24位的二进制可以精确表示几位十进制?因为lg224 = 7.22。所以单精度浮点数的精度为7,即可以表示7位有效数字。 同理,对于双精度浮点数而言,lg253 = 15.95,所以双精度浮点数的精度为15,即可以表示15位有效数字。 需要注意的是,虽然float和double能表示的整数范围比int和long long能表示的整数范围大的多,但因为浮点数无法精确表示,所以要表示整数(精确值)时,只能用整型变量来表示,不能使用浮点型变量来表示。

十三、程序验证

代码语言:javascript复制
#include <cstdio>
using namespace std;

int main()
{
    float a = 0.123456789;
    float b = 123.123456789;
    float c = 123456.123456789;
    float d = 123456789;
    float e = 123456789123456789;

    printf("%.10fn", a);
    printf("%.10fn", b);
    printf("%.10fn", c);
    printf("%.10fn", d);
    printf("%.10fn", e);

    return 0;
}

运行结果:

代码语言:javascript复制
0.1234567910
123.1234588623
123456.1250000000
123456792.0000000000
123456790519087104.0000000000

可以看出,结果中的第1行、第4行、第5行精确的有效位数为7位,第2行、第3行精确的有效位数为8位,所以单精度浮点数只能保证前7位有效数字是正确的。

0 人点赞