浮点数是 C++的第二组基本类型,它能够表示带小数部分的数字。不仅如此,浮点数的范围也比 int 更大,可以表示更大范围的数字。
我们都知道在计算机当中,所有数据本质上都是转化成二进制存储的。整数很简单,存储的就是转化成二进制之后的 01 串,那么浮点数又是如何存储的呢?
很容易猜到的是浮点数存储的结果也是二进制,但相比于整型直接转化成二进制要复杂一些。它需要先表示成下面这行式子:
这里的n
即我们要存储的浮点数,s
表示符号位,m
是尾数,而e
则是阶数。
符号位很好理解,它和整型当中的符号位一样,0 表示正数,1 表示负数。m
表示尾数,$ 1\le m < 2$。我们这么看很抽象,来看一个例子,比如3.0,转化成二进制是$(11.0)_2$,相当于$1.1*2^1$。那么,$s=1, m=1.1, e=1$。
我们了解了浮点数的表示方式,那么它又是如何存储在计算机当中的呢?这需要我们进一步地剖析其中的细节。
首先是 m,m 被定义成一个大于等于 1,小于 2 的小数。我们可以简单写成 1.xx,其中 xx 表示的就是小数的部分。
既然它总是大于等于 1,小于 2 的,那么它的个位一定是 1,我们就可以将它省略,仅仅看之后小数的部分。小数的部分,我们同样使用二进制来逼近。比如 0.625,可以表示成 0.5 + 0.125,即$2^{-1} + 2^{-3}$,表示成二进制就是$(101)_2$,只不过这里它的最高位是从-1 开始的。
以 32 位的浮点数为例,除去 1 位表示符号,8 位表示阶数之后,还有 23 位留给 m。由于我们舍掉了小数点之前的 1,所以我们的阶数是从-1 开始的,理论上等价于 24 个二进制位。
在浮点数存储当中,e 是一个无符号整数。以 32 位浮点数为例,e 一共有 8 位,可以表示 0-255。
但 e 是可以为负数的,根据 IEEE 754 的规定,e 的真实值必须再减去一个中间数。对于 8 位的 e,它的中间数是 127。比如 e 的实际值是 10,但是存储的时候需要存储成 127+10=137。
除此之外,e 还有另外三种情况:
- e 不全为 0,或全为 1 时,采用上述的规则表示
- e 全为 0 时,e 等于 1-127,有效数字 m 不再默认加上 1,这样是为了还原 0.xxx 的小数,以及接近于 0 的数
- e 全为 1 时,如果有效数字 m 全为 0,表示无穷大,如果 m 不全为 0,表示 nan(not a number)
关于 e 的规则看起来有些复杂,初看觉得有些难以理解,为什么要用减去中间值的设计,而不用符号位?后来仔细思考了一下才发现,如果引入符号位很难区分 0.xxx 以及 e 就是等于 0 的情况,虽然也可以特判处理,但就没有现在这样优雅了。
觉得上文看不懂的小伙伴可以直接略过这段,毕竟这个是浮点数的实现原理,算是很底层的内容了,C++ primer 上对于这部分也没有过多阐述。
C++当中有两种浮点数的书写方式,第一种是使用常规的小数点表示法:
double a = 1.23;
float b = 3.43;
另外一种写法是科学记数法,写成:
double a = 2.45e8;
double b = 1e-7;
2.45e8 表示$2.45 * 10^8$,e 之后可以跟正数也可以跟负数,但数字当中不能有空格。
和 C 语言一样,C++也有三种浮点数类型:float
,double
和long double
。和整型一样,这三种类型都是浮点数,只不过表示的范围不同。
浮点数的范围有两个部分综合决定,一个部分是有效数字。比如 14179 是 5 位有效数字,而 14000 只有两位,因为后面三个 0 都是填充位,有效数字的位数不依赖小数点的位置。C++当中要求,float
通常能表示 7 位有效数字,double
能表示 16 位,而long double
至少和double
一样。
另外,它们能够表达的指数范围至少是-37 到 37。一般来说,float
一共是 4 个字节 32 位,而double
是 8 个字节 64 位,当然这也取决于具体的运行环境。
关于浮点数的使用有几点注意事项,千万要注意。
- cout 输出浮点数会删除结尾的 0
- 书写浮点数常量时默认为
double
类型,如果需要强制表示为float
类型,请在结尾加上后缀 f 或者 F,如:2.34f
- 由于浮点数有精度,不能直接判断两个浮点数是否相等,很有可能得不到预期结果,正确的做法是判断精度范围,如:
double epsilon = 1e-8;
// 判断a是否和b相等
if (abs(a - b) < epsilon) {
// todo
}
判断两个浮点数 a 和 b 是否相等,等价于两者的差的绝对值小于某一个精度。
- 范围问题,如运行下列代码将得到错误的结果:
float a = 2.3e22f;
float b = a + 1.0f;
cout << b - a << endl;
输出的结果将是 0,因为 2.3e22 是一个小数点左边有 23 位的数字,加上 1 之后,就是在第 23 位加上 1。但是float
类型只能表示数字中的前 6 位或者前 7 位,表示不了这么高的精度,因此这个+1 的操作完全没有生效。
这个问题是一个大坑,一不小心就会中招,千万要小心。