本帖最后由 lcofjp 于 2017-12-10 19:02 编辑
在各种编程语言中,数值类型是最为常用的数据类型,数值类型包含两大类:整数和浮点数,整数类型比较通俗易懂,结构、取值范围以及运算过程都非常的直观,而浮点数就相对复杂了一些,本文的主要内容就是聊一聊浮点数。
在编程语言中,浮点数通常都是按着IEEE-754标准来实现的,本文以C语言为例,来探一探浮点数的坑。C语言中,浮点数类型包含单精度浮点数(float)和双精度浮点数(double),float为32位,double为64位。为了便于后面的测试,先来了解一下浮点数输入输出,在控制台程序中,通常用scanf和printf来实现输入和输出,浮点数的修饰符可以是以下之一:
f/F | 十进制小数 |
e/E | 科学计数法 |
g/G | 根据数值大小自动选择f或者e格式 |
a/A | 十六进制小数 |
在修饰符前面,还可以有一个数据类型长度修饰符,用来指明更具体的数据类型,长度修饰符有l和L,以f格式为例:
在scanf中,f对应float*,lf对应double*,Lf对应long double*,
浮点数由三个部分组成:[1]s=符号位/sign,[2]q=指数/exponent,[3]c=尾数/significand/coefficient,表示形式为:
(−1)s × c × 2^q
float和double的各部分所占位数:
| 符号位(s) | 指数(q) | 尾数(c) |
float | 1 | 8 | 23 |
double | 1 | 11 | 52 |
以float为例,float类型在内存中的存储结构看起来是这样的:
下面来了解一下浮点数各部分的求值过程:
举个例子,比如浮点数22.453125:
符号位只有一位,正数为0,负数为1,此数为正符号位为0。
整数部分和小数部分分别用二进制表示,为:
10110.011101
下一步是标准化,经过移动小数点,使小数点的左边只有一个1,也就是
1.0110011101 * 2^4
因为小数向左移动了4位,要乘以2^4保持不变。
上面的小数部分,去掉小数点左边的1,就是浮点数的尾数部分,22.453125的尾数部分就是:
0110011101,后面不足23位补0
指数部分就是4 + 127 = 131,用二进制表示就是10000011,其中另外加的127叫做指数偏差,double类型的指数偏差是1023。
那么现在22.453125的32位浮点数二进制表示已经出来了:
0 10000011 01100111010000000000000
类似于这种可以化成小数点左边只有一个1,指数部分的范围在[1,254]区间内的浮点数称为标准化浮点数(针对float类型),还有一些另类的数则是非标准化的,比如趋近于0的数以及0(下溢),趋向于正负无穷大的数(上溢):
0是一个特殊的值,无法标准化,所以用一个特殊的二进制全0来表示,当然还有一个-0,在某些语言中,+0等于-0,而在某些语言中则不相等。通常在C中是无法写出-0的字面量的。有些趋近于0的数值,在标准化后指数会下溢,也就是无法表示,此时可以使指数为0,而尾数部分不使用标准化的形式,这种数称为非标准化的。
QNaN和SNaN统称为NaN,NaN一般用于在计算过程中产生了无法表示的数值,比如0/0, 0×±∞, 任何数与NaN运算。QNaN在参与运算时通常不会产生异常,而SNaN在运算时会产生异常,所以SNaN可以用来初始化一些浮点数的内存,当这些浮点数未赋值为有效的浮点数并参与运算时,就会产生异常以指出程序的问题。NaN的指数为全1,尾数部分可以根据实现用于不同的用途,例如指示导致NaN的原因。至于是否支持QNaN和SNaN需要看编译器是否实现。
正负无穷大用于表示运算的上溢,也就是运算结果太大以至于无法表示。
为了方便测试,我写了一个浮点数构造函数,可以用来生成特定的浮点数,以及一个解析浮点数的函数,用来打印浮点数的各部分:
- void partitionFloat(float f) {
- uint32_t x = *(uint32_t*)&f;
- uint32_t e = (x >> 23) & 0xff;
- for(int i=31; i>=0; i--) {
- if(x & (1UL<<i)){
- putchar('1');
- }
- else {
- putchar('0');
- }
- if (i == 31 || i == 23) {
- putchar(' ');
- }
- }
- if ( e != 0 && e != 0xff) {
- printf("\nexponent = %d\n", e - 127);
- }
- putchar('\n');
- }
- float constructFloat(uint32_t sign, uint32_t exp, uint32_t sig /*significand*/) {
- float f = 0;
- uint32_t *p = (uint32_t*)&f;
- *p = (sign<<31) + ((exp&0xff) << 23) + (sig & (1<<23)-1);
- return f;
- }
复制代码
测试:
- float a = +0, b = -0;
- partitionFloat(a);
- partitionFloat(b);
- partitionFloat(22.453125);
- float sNaN = constructFloat(0, 255, 1);
- float qNaN = constructFloat(0, 255, 1<<22);
- float pInf = constructFloat(0, 255, 0);
- float nInf = constructFloat(1, 255, 0);
- float pZero = constructFloat(0, 0, 0);
- float nZero = constructFloat(1, 0, 0);
- float min = constructFloat(0, 1, 1);
- float max = constructFloat(0, 254, (1<<23)-1);
- printf("sNaN=%g, qNaN=%g, pInf=%g, nInf=%g, pZero=%g, nZero=%g, min=%g, max=%g\n", sNaN, qNaN, pInf, nInf, pZero, nZero, min, max);
复制代码结果:
0 00000000 00000000000000000000000
0 00000000 00000000000000000000000
0 10000011 01100111010000000000000
exponent = 4
sNaN=nan, qNaN=nan, pInf=inf, nInf=-inf, pZero=0, nZero=-0, min=1.17549e-38, max=3.40282e+38
对于这些特殊数值的判断,并不能直接使用相等运算符去判断,因为并没有对应的可比较的字面量常量去表示nan,inf等,而是应该使用标准库提供的函数,比如在math.h中,可能包含这些函数或者宏定义的声明:
fpclassify 判断浮点数的类型,返回结果可能是:FP_INFINITE,FP_NAN,FP_NORMAL,FP_SUBNORMAL,FP_ZERO
isfinite 是否有限大小
isinf 是否是无穷大
isnan 是否是NaN
isnormal 是否标准化
signbit 符号位
实例:
- printf("2.2 is finite: %d\n", isfinite(2.2));
- printf("signbit of -2.2 is: %d, and 2.2's is: %d\n", signbit(-2.2), signbit(2.2));
- printf("%d,%d,%d,%d,%d\n", fpclassify(sNaN), fpclassify(qNaN), fpclassify(pInf), fpclassify(0.0), fpclassify(2.2));
复制代码结果:
2.2 is finite: 1
signbit of -2.2 is: 1, and 2.2's is: 0
1,1,2,3,4
在float.h中,编译器提供了一些浮点数的极值,以及各组成部分相关的一些常量。
使用浮点数的注意事项:
1. 绝大多数实数无法用浮点数精确表示,在转换为浮点数的过程中要进行舍入或者截断,至于是舍入还是截断要参考具体实现。
相对误差=|xc - x|/|x|, 如果xc与x的相对误差小于某个非常小的值,则可以认为这两个数近乎相等。
3. 尽量不要让两个相近的浮点数去做减法运算,这样会大大降低浮点数的精度,而应该想办法转换为其他运算。
比如说,表达式(1-cosx)/sin2x,在x接近与0时,结果误差就会非常大,而转换成1/(1+cosx)去计算,误差就会非常小。
此内容由EEWORLD论坛网友lcofjp原创,如需转载或用于商业用途需征得作者同意并注明出处