如何将浮点数转换为字节?

12

我正在使用 HIDAPI 向 USB 设备发送一些数据。这些数据只能以 byte 数组的形式发送,并且我需要在这个数据数组中发送一些 float 数字。我知道浮点数有 4 个字节。所以我想这样做可能会有效:

float f = 0.6;
char data[4];

data[0] = (int) f >> 24;
data[1] = (int) f >> 16;
data[2] = (int) f >> 8;
data[3] = (int) f;

后来我所要做的就是:

g = (float)((data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]) );

但是测试结果显示,类似于data[0] = (int) f >> 24;这样的行总是返回0。我的代码有什么问题,我该如何正确地执行它(即将一个float内部数据分成4个char字节,并稍后重新构建相同的float)?


编辑:

我用以下代码成功实现了这一点:

float f = 0.1;
unsigned char *pc;
pc = (unsigned char*)&f;

// 0.6 in float
pc[0] = 0x9A;
pc[1] = 0x99;
pc[2] = 0x19;
pc[3] = 0x3F;

std::cout << f << std::endl; // will print 0.6

*(unsigned int*)&f = (0x3F << 24) | (0x19 << 16) | (0x99 << 8) | (0x9A << 0);

我知道memcpy()是更"干净"的方式,但我认为这种方式性能会更好。


1
(int)f >> 24 返回 0 的原因是,强制转换为 intf 在第一次就等于了 0:强制转换将浮点数向下取整。这是未定义的行为,但要以这种 hacky 的方式做到这一点,您需要像 *(int*)&f >> 24 这样的东西。 - Andrey Mishchenko
6个回答

25

你可以这样做:

char data[sizeof(float)];


float f = 0.6f;

memcpy(data, &f, sizeof f);    // send data


float g;

memcpy(&g, data, sizeof g);    // receive data
为了使这个功能生效,两台机器需要使用相同的浮点表示方法。
正如评论中所指出的那样,您不一定需要进行额外的memcpy操作;相反,您可以将f直接作为一个字符数组(带有任何符号)来处理。然而,在接收端仍然需要执行memcpy操作,因为您不能将任意字符数组视为浮点数!示例:
unsigned char const * const p = (unsigned char const *)&f;
for (size_t i = 0; i != sizeof f; ++i)
{
    printf("Byte %zu is %02X\n", i, p[i]);
    send_over_network(p[i]);
}

我喜欢这个答案,但我很好奇,有没有其他方法可以在浮点数上进行字节级访问? - Michel Feinstein
1
@mFeinstein 0.6ffloat 类型的常量,0.6double 类型的常量。double 类型的常量会自动转换为 float(但是一些简单的编译器可能会为 f = 0.6 生成更差的代码,并且某些平台可能会有不同的舍入方式)。还有其他方法可以进行字节级访问,但在这里使用 memcpy 是最好的方式。 - Gilles 'SO- stop being evil'
@Gilles:是的,你可以直接将f视为一个字符数组:char const * data = (char const *)&f;,现在可以使用data[i]来表示范围为i的值。 - Kerrek SB
有些情况下,无法区分浮点数、双精度和整型常量可能会让你陷入麻烦,因此总是指定“f”或“d”后缀并不是一个坏主意。话虽如此,我必须承认,只有在需要确保它是正确的类型或需要确保读者理解它是什么类型时,我才会这样做。 - keshlam
@mFeinstein:检查我的答案,看如何访问浮点数中的单个字节。 - pablo1977
显示剩余2条评论

9

在标准C中,保证任何类型都可以作为字节数组进行访问。当然,一种直接的方法是使用联合:

 #include <stdio.h> 

 int main(void)
 {
    float x = 0x1.0p-3; /* 2^(-3) in hexa */

    union float_bytes {
       float val;
       unsigned char bytes[sizeof(float)];
    } data;

    data.val = x;
    for (int i = 0; i < sizeof(float); i++) 
          printf("Byte %d: %.2x\n", i, data.bytes[i]);

    data.val *= 2;   /* Doing something with the float value */
    x = data.val;    /* Retrieving the float value           */
    printf("%.4f\n", data.val);

    getchar();
 }

正如您所看到的,根本没有必要使用memcpy或指针...
联合体方法易于理解,标准且快速。
编辑。
我将解释为什么这种方法在C(C99)中是有效的。
[5.2.4.2.1(1)]一个字节有CHAR_BIT个位(一个大于等于8的整数常量,在几乎所有情况下都是8)。
[6.2.6.1(3)] unsigned char类型使用其所有位来表示对象的值,该值是一个非负整数,在纯二进制表示中。这意味着没有填充位或用于任何其他奇怪目的的位。(对于signed char或char类型,不保证相同的事情)。
[6.2.6.1(2)]每种非位域类型在内存中都表示为一系列连续的字节。
[6.2.6.1(4)](引用)“以任何其他对象类型的非位字段对象存储的值由n× CHAR_BIT位组成,其中n是该类型的对象的大小,以字节为单位。该值可以复制到unsigned char [n]类型的对象中(例如,通过memcpy);[...]”
[6.7.2.1(14)]指向结构对象(尤其是联合体)的指针经过适当转换后指向其初始成员。 (因此,在联合体开头没有填充字节)。
[6.5(7)]可以通过字符类型访问对象的内容:
一个对象的存储值只能由具有以下类型之一的lvalue表达式访问: -与对象的有效类型兼容的类型, -与对象的有效类型兼容的类型的限定版本, -与对象的有效类型相应的有符号或无符号类型, -与对象的有效类型的限定版本相应的有符号或无符号类型, -包括上述类型之一在其成员中的聚合或联合类型(包括子聚合或包含联合体的成员,递归地),或 -字符类型
更多信息:

谷歌小组中的讨论
类型转换

编辑2

C99标准的另一个细节:

  • [6.5.2.3(3) footnote 82] 允许使用类型转换:

如果用于访问联合对象内容的成员与用于最后存储对象中值的成员不同,则将该值的对象表示的适当部分重新解释为新类型的对象表示,如6.2.6所述(有时称为“类型转换”)。这可能是陷阱表示。


3
请注意,虽然这在C语言中可能是有效的(我认为是C99,但不是C89?),但在C++中这将是未定义行为。以防有任何C++用户路过并看到这个。 - Kerrek SB
当然,那只是一句话。有时候人们认为可以将一种语言的东西应用到另一种语言中,所以我只是想保险起见。C对于访问内存比C++更加宽松;我没有标准参考来证明这是有效的,但我相信你是正确的。 - Kerrek SB
@KerrekSB:请注意,我正在使用unsigned char[]来别名化float对象。这非常具体化,我认为在C++中也是标准的。如果使用不同于unsigned char的类型,则会产生未定义的行为。请参阅此讨论 - pablo1977
1
@KerrekSB 我在这个答案中遇到了通过联合体进行类型转换的雷区,我链接了一些更好的讨论。Pascal Cuoq的解释和他链接的DR支持自C89以来它是合法的。C++的情况并不清楚,我倾向于它是未定义的,但也可能不是。 - Shafik Yaghmour
1
@KerrekSB 我指的是C和C++中联合的目的,我还链接了这个访问未激活的联合成员-未定义? - Shafik Yaghmour
显示剩余12条评论

1
C语言保证任何类型的值都可以被访问为字节数组。字节的类型是unsigned char。以下是将float复制到字节数组的低级方法。sizeof(f)是用于存储变量f的值的字节数;您也可以使用sizeof(float)(您可以将sizeof传递给变量或更复杂的表达式,或其类型)。
float f = 0.6;
unsigned char data[sizeof(float)];
size_t i;
for (i = 0; i < sizeof(float); i++) {
    data[i] = (unsigned char*)f + i;
}

memcpy函数或memmove函数正是做这件事情的(或者是它们的优化版本)。

float f = 0.6;
unsigned char data[sizeof(float)];
memcpy(data, f, sizeof(f));

你甚至不需要创建这个副本。你可以直接将指向浮点数的指针传递给写入到USB的函数,并告诉它要复制多少字节(sizeof(f))。如果函数接受除void*以外的指针参数,则需要进行显式转换。
int write_to_usb(unsigned char *ptr, size_t size);
result = write_to_usb((unsigned char*)f, sizeof(f))

请注意,这仅适用于设备使用相同的浮点数表示形式,这是常见但不普遍的。大多数机器使用IEEE浮点格式,但您可能需要切换字节序。
关于你的尝试有什么问题: >> 操作符作用于整数。 在表达式(int) f >> 24中,f被强制转换为int;如果你写了没有强制转换的f >> 24,那么f将自动转换为int。 将浮点值转换为整数通过截断或四舍五入来近似它(通常向0舍入,但规则取决于平台)。 四舍五入到整数的0.6是0或1,因此 data [0] 是0或1,而其他都是0。

你需要操作float对象的字节,而不是它的值。

¹ 不包括在C中无法实际操作的函数,但包括函数指针自动衰减成的函数。


能否在一行中完成,而不使用 for 循环?使用 <<| 来分割字节? - Michel Feinstein
@mFeinstein,使用<<和其他整数操作无法帮助你。请查看我的编辑。 - Gilles 'SO- stop being evil'
是的,我明白了,但我认为将浮点数转换为无符号字符数组可能会奏效。我只是试图避免不必要的代码,因为这将在中断中运行。但如果开始变得复杂,那么使用memcpy几乎是相同的,我会使用它。 - Michel Feinstein
@mFeinstein 你不能将类型转换为数组类型,但将指向float的指针转换为 unsigned char * 基本上是同样的操作。 - Gilles 'SO- stop being evil'

0
假设两个设备对浮点数的表示方式相同,那么为什么不直接使用memcpy呢?即
unsigned char payload[4];
memcpy(payload, &f, 4);

因为这些字节将在微控制器中被读取,我不确定微控制器库中是否有memcpy函数...但现在我看到它是存在的。 - Michel Feinstein

0

如果你控制双方,最安全的方法是发送某种标准化表示形式...虽然这不是最有效的方法,但对于小数量来说也不算太糟糕。

hostPort writes char * "34.56\0" byte by byte
client reads char * "34.56\0" 

然后使用库函数atofatof_l将其转换为浮点数。

当然,这并不是最优化的方法,但它肯定很容易调试。

如果你想要更优化和创造性,首先字节表示长度,然后指数,然后每个字节表示2个小数位...所以

34.56变成char array[] = {4,-2,34,56};像这样的东西是可移植的...我只是尽量避免传递二进制浮点表示...因为它可能会很快变得混乱。


这对我的需求来说会非常麻烦,因为我有一个微控制器来接收数据,而且性能也不是最好的。 - Michel Feinstein

-2

将浮点数和字符数组合并可能更安全。将浮点成员放入其中,取出4个(或任何长度)字节。


牛。这就是 联合 的全部目的... 将数据以一种格式放入并将未经改变的位作为另一种格式拉出来。 - Phil Perry
1
不,这不是联合的目的。联合的目的是在不同的时间使用相同的内存片段来存储不同的数据。在C89中,行为是实现定义的。在C++中,GCC利用此进行优化;我认为某些版本也在C中这样做,但查找后发现我错了:这在GCC中是安全的。C11也已更改以使其定义,因此在当前编译器上实际上可能是安全的,即使它们不完全符合C11。 - Gilles 'SO- stop being evil'
错误。联合(Union)的主要用途是允许以不同的方式访问给定数据块中的位和字节,例如作为浮点数和作为字符(字节)数组。它可以被用于在不同时间将内存用于不同的目的来节省一些空间,但这是次要的用途。 - Phil Perry
1
不,你完全搞错了。在早期标准中,并未明确规定联合体中的类型游戏(尽管被广泛支持)。联合体的主要目的是在同一块内存空间中存储不同且没有关联的对象(通常伴随着枚举或整数对象,用于指示联合体的哪个字段当前有效,但不一定必须如此)。 - Gilles 'SO- stop being evil'
1
@Gilles,您在链接到的答案https://dev59.com/9mgu5IYBdhLWcg3wYF-3#11996970中提到的脚注已经存在于C99TC3中。如果您相信标准委员会是不可错的,那么这意味着它一直存在于C99标准中,尽管没有明确表达。根据这种解释,C89、C99和C11都不会将使用联合体进行类型转换定义为未定义行为。 - Pascal Cuoq
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接