我正在阅读http://beej.us/guide/bgnet/html/#serialization教程的序列化部分。
我正在审查将数字编码为可移植的二进制形式的代码。
#include <stdint.h>
uint32_t htonf(float f)
{
uint32_t p;
uint32_t sign;
if (f < 0) { sign = 1; f = -f; }
else { sign = 0; }
p = ((((uint32_t)f)&0x7fff)<<16) | (sign<<31); // whole part and sign
p |= (uint32_t)(((f - (int)f) * 65536.0f))&0xffff; // fraction
return p;
}
float ntohf(uint32_t p)
{
float f = ((p>>16)&0x7fff); // whole part
f += (p&0xffff) / 65536.0f; // fraction
if (((p>>31)&0x1) == 0x1) { f = -f; } // sign bit set
return f;
}
我在
p = ((((uint32_t)f)&0x7fff)<<16) | (sign<<31); // whole part and sign
这一行遇到了问题。根据原始代码注释,此行提取整个部分和符号,下一行处理分数部分。
然后,我找到了一张有关
float
如何在内存中表示的图像,并手动开始了计算。从维基百科Single-precision floating-point format:
因此,我假设整个部分 == 指数部分。
但是,如果基于上面的图像,则此
(uint32_t)f)&0x7fff)<<16)
将获取分数部分的最后的的。现在我很困惑,哪里出了错?
最佳答案
重要的是要意识到这段代码不是什么。此代码对float
值的各个位不执行任何操作。 (如果确实如此,它将不会像它声称的那样是可移植的并且与机器无关。)并且它创建的“便携式”字符串表示形式是固定点,而不是浮点。
例如,如果使用此代码转换数字-123.125
,我们将获得二进制结果
10000000011110110010000000000000
或十六进制
807b2000
现在,这个数字
10000000011110110010000000000000
是从哪里来的?让我们将其分解为符号,整数和小数部分:1 000000001111011 0010000000000000
符号位为1,因为我们的原始数字为负。
000000001111011
是123的15位二进制表示。0010000000000000
是8192。8192来自何处?好吧,8192÷65536是0.125,这是我们的小数部分。 (更多有关此内容。)代码是如何做到的?让我们逐步介绍它。
(1)提取符号。这很容易:这是普通的测试
if(f < 0)
。(2)提取整数部分。这也很容易:我们将浮点数
f
转换为unint32_t
。当您将浮点数转换为C语言中的整数时,其行为非常明显:它将丢弃小数部分,并为您提供整数。因此,如果f
为123.125,则(uint32_t)f
为123。(3)提取分数。由于我们已经有了整数部分,因此可以通过从原始浮点数
f
开始并减去整数部分来隔离分数。即123.125-123 = 0.125。然后我们将小数部分乘以65536,即216。我们乘以65536而不乘以其他数字可能并不明显。从某种意义上说,使用什么数字都没有关系。这里的目标是取一个小数点
f
并将其转换为两个整数a
和b
,以便我们稍后可以再次恢复该小数点f
(尽管可能近似)。我们稍后将再次恢复分数f
的方法是通过计算a + b / x
x
在哪里,还有其他一些数字。如果我们为x
选择1000,则将123.125分为a
和b
值123和125。我们为x
选择65536或216,因为这可以最大程度地利用分配给我们的16位我们表示中的小数部分。由于x
是65536,因此b
必须为某个数字,我们可以将其除以65536,以获得0.125。因此,从b / 65536 = 0.125
开始,通过简单的代数,我们有了b = 0.125 * 65536
。有道理?无论如何,现在让我们看一下执行步骤1、2和3的实际代码。
if (f < 0) { sign = 1; f = -f; }
十分简单。如果
f
为负,则我们的符号位将为1,并且我们希望其余代码在f
的正版本上运行。p = ((((uint32_t)f)&0x7fff)<<16) | (sign<<31);
如前所述,此处的重要部分是
(uint32_t)f
,它只是获取f
的整数(整数)部分。位掩码& 0x7fff
提取其中的低15位,并丢弃其他任何内容。 (这是因为我们的“便携式表示形式”仅为整数部分分配了15位,这意味着不能表示大于215-1或32767的数字)。移位<< 16
将其移至最终unint32_t
结果的上半部分,即它所属的位置。然后| (sign<<31)
接受符号位并将其放在它所属的高位位置。p |= (uint32_t)(((f - (int)f) * 65536.0f))&0xffff; // fraction
在这里,
(int)f
重新计算f
的整数(整数)部分,然后f - (int)f
提取分数。如上所述,我们将其乘以65536。可能仍然存在一个小数部分(即使在乘法之后也是如此),因此我们再次转换为(uint32_t)
将其丢弃,仅保留整数部分。我们只能处理16位小数,因此我们可以使用& 0xffff
提取这些位(丢弃其他任何内容),尽管这是不必要的,因为我们从小于1的正小数开始并将其乘以65536,所以我们应该得出结果正数小于65536,即我们不应该有一个不完全适合16位的数字。最后,p |=
操作将我们刚刚计算出的这16位填充到p
的低位一半中,我们已经完成了。
关于c - 一个简单的 float 序列化示例的问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58938528/