【C】浮点数

113 阅读3分钟

小数在C语言中称为浮点数(floating-point number),有float、double和long double三种类型。相比于整数,浮点数能表示更广范围的、更精确的数,常用于数学和金融领域。

浮点数的数值表示:1.2311.5E16

C语言的浮点数遵循IEEE 754标准,其规格化的表示为:

(1)sign×1.mantissa×2exponentoffset=(1)sign×(1+mantissa223)×2exponent127\mathbf{(-1)^{sign} \times 1.mantissa \times 2^{exponent - offset} = (-1)^{sign} \times (1+\frac{mantissa}{2^{23}}) \times 2^{exponent - 127}}

mantissa223\mathbf{\frac{mantissa}{2^{23}}}表示将mantissa的小数点移动至最前方。

C语言中的float可以叫做单精度浮点数,存储长度为4 byte。

系统通常使用32位bit存储单精度浮点数,其中,1位表示符号位sign,8位表示指数或阶码exponent的值及其符号,23位表示尾数mantissa或称为有效数字significand。符号位控制正负;指数控制了小数点的最后位置,决定了表示范围;尾数控制有效数字,也就是表示精度。指数使用移码1表示,偏置量为127;尾数使用原码表示,隐藏了1位整数部分1

小数的有效数字是指从左边第一个非零数字开始,一直到最后一个数字。例如,0.01230的有效数字位数为4,1.02的有效数字位数为3。

C标准规定float类型必须至少能表示6位有效数字,并且至少提供1037103710^{-37} \sim 10^{37}​这个范围的数。

那float类型的实际精度是多少?

大部分的验证法如下:

由于log102236.92log_{10}{2^{23}} \approx 6.92,得出精度为6-7位。

较少的验证法:

我们说的有效数字是在十进制情况下。假设mantissa达到最小,即最后一位为1,值为2230.0000001192^{-23} \approx 0.000000119,第二小的为2220.0000002382^{-22} \approx 0.000000238,其次为2210.0000004762^{-21} \approx 0.000000476​,加上隐藏位1,这三个数之间的数是float类型无法进行表示的,也就是说,到了精度为8的时候,是无法表示所有数在内的,这只是部分精确。得出精度为7位。

本文考虑在计算有效位数的时候,加上对阶码的分析:

假设小数部分为1.xxx1.xx···x,计算阶码部分对它的影响后,变为0.001xxx0.00···1xx···x,最小正数为2(12623)=0.0010009.8607613e322^{-(126-23)} = 0.00···100···0 \approx 9.8607613e-32,其次为2103+21269.8607624e322^{-103}+2^{-126} \approx 9.8607624e-32​​。浮点数越小,变化幅度越小,密度越大。这样看能达到的最少有效位数是6。

怎么做是对的呢?

然后是float类型的实际范围。当尾数和指数达到最大值时,达到float范围边界。由于指数为-127、128时有特殊用途2,所以指数最大为127,对应移码为1111 1101;尾数全1时最大。故±1.1111×2127±3.4E38\pm 1.111···1 \times 2^{127} \approx \pm 3.4 E 38​为float范围边界。

C语言的double类型是双精度浮点型,要求的最小范围与float相同,但标准扩展了最小有效数字位数到了10位。并且,增加了32位额外bit,既可以补充指数,也可以补充尾数,或者两者皆有之。无论哪种做法,至少产生13位有效数字。根据IEEE 754标准,双精度浮点型使用64位来存储,其中有1位用于符号位,11位用于指数部分,剩下的52位用于尾数部分。double的实际精度为16,验证方式同上。double的表示范围为1.79E308+1.79E308-1.79\text{E308} \sim +1.79\text{E308}

疑问:为什么无论哪种做法,至少产生13位有效数字?

一般来说,CPU处理单精度浮点数的速度比处理双精度浮点数快。

为了更高精度,C标准还提供了long double类型。其具体实现取决于系统,但是至少会比double更加精确。

Footnotes

  1. 移码又叫增码或偏置码,符号位使用1表示正数,使用0表示负数,数值部分与补码相同。

  2. 128对应的阶码 = 1000 0000 + 0111 1111 = 111111111111 1111​,-127的对应的阶码 = -0111 1111 + 0111 1111 = 0000 0000。