浮点小数是程序中经常使用的一种数据类型,其使用非常广泛,特别是在科学计算、工程领域以及计算机图形学、人工智能等领域。
1. 原理
在计算机中,浮点数常常采用 IEEE 754 标准来表示。这个标准定义了两种浮点数格式:单精度(32位)和双精度(64位)。
- 在单精度浮点数中,通常使用1位表示符号、8位表示指数和23位表示尾数;
- 在双精度浮点数中,通常使用1位表示符号、11位表示指数和52位表示尾数。
那么,当我们拿到一个小数时,是如何存储到这 32 个二进制位中?
我们以 3.75 为例,来说明下存储过程。
- 将 3.75 转换为二进制表示:\(11.11\)
- 二进制表示转换为科学计数法:\(1.111*2^1\)
- 我们这个数是正数,所以最高位符号位放 0,反之如果是负数,则最高位符号位放 1
- 中间 8 个比特位存储指数,即:\(2^1\)中的1,但是并不是直接把 1 放进去,这 8 个比特位是不包含符号位的,其只能表示正数,但是指数是可能存在负数的,所以,将其值加上 127 转换为正数即可,处理的时候再减去 127 就行。所以,这 8 个比特位实际存储的是:1+127=128,对应的二进制为:10000000
- 尾数部分是:1.111,这部分的值都是 1.xxx,所以为了节省了一个二进制位,这里只存储 111,处理的时候加上前面的 1 即可。
- 所以,3.75 实际在内存中存储为:0 10000000 11100000000000000000000
除二取余,倒序排列,高位补零
- 将要转换的整数不断除以2,直到商为0。
- 每次除法得到的余数(0或1)就是二进制数的一个位数,从下往上排列得到最终的二进制表示。
举个例子,将整数27转换为二进制:
步骤1:27 ÷ 2 = 13 余 1
步骤2:13 ÷ 2 = 6 余 1
步骤3:6 ÷ 2 = 3 余 0
步骤4:3 ÷ 2 = 1 余 1
步骤5:1 ÷ 2 = 0 余 1
将余数从最后一步开始排列,得到二进制数为 11011。
乘二取整
- 将小数乘以2,取结果的整数部分作为二进制数的一个位数。
- 将小数部分保留,再次乘以2,重复以上步骤直到小数部分为0或者达到所需的精度。
举个例子,将小数0.625转换为二进制:
步骤1:0.625 × 2 = 1.25,取整数部分1,小数部分保留为0.25
步骤2:0.25 × 2 = 0.5,取整数部分0,小数部分保留为0.5
步骤3:0.5 × 2 = 1,取整数部分1,小数部分为0,转换结束。
因此,0.625 的二进制表示为0.101。
我们通过一个程序例子来获得 3.75 在内存中真实的二进制位是不是和我们分析的一样,如下代码:
#include <iostream> #include <stack> void test() { float value = 3.75; // 初始化 p 指针指向 value 的低地址位置(也是低位字节位置) unsigned char *p = (unsigned char *)&value; // 输出的二进制位是由低到高,通过栈翻转下输出 std::stack<unsigned int> bit_values; // 遍历每一个字节 for (unsigned int i = 0; i < sizeof(value); ++i) { // 从右向左获得每一个字节(8bit)的数据 unsigned char temp = p[i]; // 用于位与运算提取每个位的值 unsigned char mask = 0x1; for (unsigned int j = 0; j < 8; ++j) { // 提取最低位的二进制位值 unsigned int bit_value = temp & mask; // 存储二进制位置到栈容器中 bit_values.push(bit_value); // 当前字节数据向右移动一个位置 temp >>= 1; } } while (!bit_values.empty()) { printf("%u", bit_values.top()); bit_values.pop(); } } int main() { test(); }
程序输出结果:
# 输出结果 01000000011100000000000000000000 # 手算结果 01000000011100000000000000000000
[/expand]
2. 浮动
浮点数中的 “浮点” 指的是小数点可以 “浮动”,即可以在数字中的不同位置。例如
- 浮点数 1234.5 表示为 1.2345 × 10^3。在这个表示中,1.2345 是尾数,而 10^3 是指数。这个指数表示了小数点向右移动了3 位;
- 浮点数 12.345 表示为 1.2345 × 10^1。在这个表示中,1.2345 是尾数,而 10^1 是指数。这个指数表示了小数点向右移动了 1 位。
3. 精度
浮点数这种存储原理使其能够表示非常大或非常小的数值范围,但也引入了一些精度问题,因为在有限的位数内表示无限的实数范围是不可行的,这可能导致浮点数的舍入误差。
指数位可以为正负值,如果是一个很小的负数,例如:-127,则表示小数点向左移动了 127 位,这将会是一个非常小的数值。反之,如果是一个较大的正数,例如:127,则表示小数点向右移动了 127 位,这将会是一个非常大的数字。
尾数部分存储了数据中真实的各位的值,但是由于其位数有限,能表示的数值范围有限,故而又存在精度的问题。尾数的位数越多,则能表示的精度越高。
4. 缺点
- 运算复杂性:浮点数的计算通常由专门的浮点数处理单元(FPU)或浮点数部件执行。这些部件需要复杂的电路设计和逻辑,以支持浮点数的加法、减法、乘法、除法等操作,这就导致浮点数运算通常需要更多的指令和周期来完成,与整数运算相比,这增加了计算的开销。
- 浮点数精度:浮点数通常使用有限的位数来表示,例如单精度浮点数使用32位表示,双精度浮点数使用64位表示。这种有限精度可能会导致舍入误差,并降低计算的准确性。
- 数据存储开销:浮点数通常需要 4 或 8 个字节存储,对于存储海量浮点数场景,开销是非常昂贵。