基础笔记2 —— 不损失精度的前提下浮点数拆分成整型的方法浅析
说明
浮点数是不能进行位操作的,这就导致在进行数据拆分的时候非常麻烦。如果将其转换成整型,常用的办法是使用强制转换,虽然在原理上是利用不同的数据类型去读相同的二进制内容,但实际使用过程中往往会伴随着精度损失。
而且在一些控制场合,数据传输过程中需要将浮点数拆分成多个整型或者其他类型的数来进行传输,例如某控制器通过Modbus通信协议进行数据传输,但其寄存器只支持让你压short型的数据,你要在不损失精度的情况下,用2个short型数据去表示1个float型数据,我们应该怎么做呢?
数据在内存中的存储方式
1.概念
我们先来捋一遍概念,抛开编程语言来说,数据有8种基本类型:byte、short、int、long、float、double、bool、char,在进行拆分之前我们需要先了解一下各种数据类型的属性,以便能对数据进行正确的拆分。
byte
- 8位、有符号的以二进制补码表示的整数
- min : -128(-2^7)
- max: 127(2^7-1)
- default: 0
short
- 16位、有符号的以二进制补码表示的整数
- min : -32768(-2^15)
- max: 32767(2^15 - 1)
- default: 0
int
- 32位、有符号的以二进制补码表示的整数
- min : -2,147,483,648(-2^31)
- max: 2,147,483,647(2^31 - 1)
- default: 0
long
- 64位、有符号的以二进制补码表示的整数
- min : -9,223,372,036,854,775,808(-2^63)
- max: 9,223,372,036,854,775,807(2^63 -1)
- default: 0
float
- 单精度、32位、符合IEEE 754标准的浮点数
- float 在储存大型浮点数组的时候可节省内存空间
- 浮点数不能用来表示精确的值,如货币
- default: 0.0f
- 有效小数位 6位
double
- 双精度、64位、符合IEEE 754标准的浮点数
- 浮点数的默认类型为double类型
- double类型同样不能表示精确的值,如货币
- default: 0.0d
- 有效小数位 14位
char
- char类型是一个单一的 16 位 Unicode 字符
- 最小值是 \u0000(即为0)
- 最大值是 \uffff(即为65,535)
- char 数据类型可以储存任何字符
bool
- bool数据类型表示一位的信息
- 只有两个取值:true 和 false
- 这种类型只作为一种标志来记录 true/false 情况
对于其他概念,可以参考这篇博文1。
2.浮点数在内存中的存储方式
深入的研究请参考《IEEE754》标准。
一个浮点数在内存中由3部分来表达:符号位、底数m和指数e,值得注意的是它们都使用二进制来表示。
- 底数部分:使用2进制数来表示此浮点数的实际值。
- 指数部分:占用8-bit的二进制数,可表示数值范围为0-255。
但是指数应可正可负,所以IEEE规定,此处算出的次方须减去127才是真正的指数。所以float的指数可从 -126到128.底数部分实际是占用24-bit的一个值,由于其最高位始终为 1 ,所以最高位省去不存储,在存储中只有23-bit。到目前为止, 底数部分 23位 加上指数部分 8位 使用了31位。那么前面说过,float是占用4个字节即32-bit,那么还有一位是干嘛用的呢? 还有一位,其实就是4字节中的最高位,用来指示浮点数的正负。
- 符号位:当最高位是1时,为负数,最高位是0时,为正数。
按上述表达方法,浮点数的存储格式如下:
| Address+0 | Address+1 | Address+2 | Address+3 | |
|---|---|---|---|---|
| 内容 | SEEE EEEE | EMMM MMMM | MMMM MMMM | MMMM MMMM |
其中
- S 表示浮点数正负,1为负数,0为正数
- E是指数加上127后的值的二进制数
- M是24-bit的底数(只存储23-bit)
值得注意的是,对于浮点数为0时,指数和底数都为0,上述公式不成立(2的0次方为1),对于这个特例,一般不需要人为去干扰,编译器会自动识别
具体转换例子可参考这篇文章2。
有了前面的基础,我们接下来去试着拆分一下:
思路1:内存拷贝
因为所有数据在计算机中都是以二进制的形式储存的,因此可以考虑使用C标准库提供的memcpy将float拷贝到缓冲区中再编辑。 (实践证明不太可行,虽然原理上行得通)
//验证思路是否正确
#include <iostream>
#include<string.h>
using namespace std;
int main()
{
float data1 = 123.456;
unsigned int uiData;
float data2;
memcpy(&uiData, &data1, 32);
memcpy(&data2, &uiData, 32);
cout << "转成无符号整型:" << uiData << endl;
cout << "十六进制表示:" << hex << uiData << endl;
cout << "检验有无精度损失:" << hex <<data2 << endl;
return 0;
}
乍一看,运行结果证明我们的思路是可行的,但是我们在运行一次看看:

两次运行结果不一样,但是为什么转回去还是一样的?原因在于我们直接通过copy内存实现的,本质应该类似于指针?(抱歉,这块儿我也不是太懂),这样就相当于我们直接把data1赋值给了data2,因此两次运行结果是一样的。
注意:把float变量的内存内容拷贝给一个其他类型的变量时,编译器会默认使用的是强制转换,因此可能会伴随着精度损失。
思路2:利用指针
利用指针去读取浮点数变量的内存内容,即换种数据类型解读方式去解读这个浮点数的变量内容。
先上代码:
#include<iostream>
#include<stdio.h>
using namespace std;
int main()
{
float a = 17.625;
short *b = (short *)&a;
//此处需要注意,由于CPU架构不同,可能会采用不同的存储方式,也就是常说的大小端存储问题
cout << " 高16: " << hex << b[1] << " 低16: " << hex <<b[0] <<endl;
//在某些设备上运行此程序,可能会出现的情况是高16位和低16位整好相反
//cout << " 高16: " << hex << b[0] << " 低16: " << hex <<b[1] <<endl;
float *c = (float*)b;
cout << *c << endl;
//------------------------------------
// 两个short型数据合成一个float
short data[2];
data[0]=b[0];
data[1]=b[1];
float *text_data = (float*)b;
cout<<"合成测试:"<<*text_data<<endl;
return 0;
}

至此,完成了在不损失精度的前提下把浮点数拆成两个short型数据的工作。
关于大小端问题,参见本博客《关于大小端数据存储方式及测试说明》。
代码
#include <iostream>
#include<stdio.h>
using namespace std;
short* InsertIntVarToModbusShortArr(int iVal)
{
short spArr[2];
//小端分解
// *spArr = (short) ((iVal >> 16) & 0xFFFF);
// *(spArr + 1)= (short)(iVal & 0xFFFF);
//大端分解
spArr[0] = (short)(iVal & 0xFFFF);
spArr[1] = (short)((iVal >> 16) & 0xFFFF);
return spArr;
}
short* InsertFloatVarToModbusShortArr(float fVal)
{
//待优化
short spArr[2];
short* spArrTemp = (short*)&fVal;
spArr[0] = spArrTemp[0];
spArr[1] = spArrTemp[1];
return spArr;
}
float InsertModbusShortArrToFloatVar( short* spArr)
{
short dataTemp[2];
dataTemp[0] = *spArr;
dataTemp[1] = *(spArr + 1);
return *((float*)dataTemp);
}
int main()
{
int data1 = 8;
float data = 17.625;
short a[2] = {0,16781};
short *b;
float c;
short *data2;
data2 = InsertIntVarToModbusShortArr(data1);
cout << "InsertIntVarToModbusShortArr测试:data[1]=" << data2[1] << " data[0]=" << data2[0] << endl;
b = InsertFloatVarToModbusShortArr(data);
cout << "InsertFloatVarToModbusShortArr测试:b[1]=" << b[1] << " b[0]=" << b[0] << endl;
c = InsertModbusShortArrToFloatVar(a);
cout << "InsertModbusShortArrToFloatVar测试:c=" << c << endl;
return 0;
}