将大端序浮点数转换为小端序

26

有没有可能将大端序的浮点数转换为小端序?我从一个PowerPC平台得到了一个大端序值,我要通过TCP发送给一个Windows进程(小端序)。这个值是一个浮点数,但是当我将值memcpy到Win32浮点类型中,然后调用_byteswap_ulong函数对该值进行字节交换时,我总是得到0.0000?我做错了什么?


请看这个问题:https://dev59.com/wXI-5IYBdhLWcg3wlpQM - Drakosha
认为它们都是IEEE格式,但你应该再确认一下。 - Mark Ransom
如果您不调用 _byteswap_ulong,会发生什么? - Maciej Hehl
不要实现这样的东西。请查看boost::endian: http://www.boost.org/doc/libs/1_64_0/libs/endian/doc/index.html - morteza
@morteza的回答是他尝试但未能实现删除线,因为boost::endian中的浮点支持已被移除,因为它无法正常工作。 - AnotherParker
11个回答

41

简单地反转四个字节就行了

float ReverseFloat( const float inFloat )
{
   float retVal;
   char *floatToConvert = ( char* ) & inFloat;
   char *returnFloat = ( char* ) & retVal;

   // swap the bytes into a temporary buffer
   returnFloat[0] = floatToConvert[3];
   returnFloat[1] = floatToConvert[2];
   returnFloat[2] = floatToConvert[1];
   returnFloat[3] = floatToConvert[0];

   return retVal;
}

10
这是合法的 C 代码,没有任何编译器应该破坏它。 我已经在 VC6、Visual Studio 2008、Visual Studio 2010 和 c++ Builder 2010 中测试过了。这些编译器都没有破坏这段代码。 - Gregor Brandt
26
@Tomek:这不违反严格别名规则。C和C++都明确允许将任何类型的对象作为char数组(因此通过char*)进行访问。从您发布的链接中得到的“通过联合体转换”技巧会导致未定义的行为(从联合体的成员读取而不是最后一个写入的成员会导致未定义的行为)。 - James McNellis
12
@Tomek:对你来说好坏并不重要;重要的是它的行为是否定义良好。通过char *重新解释是被定义良好的(参见C++03 §3.10/15)。而联合体黑科技则不是(参见C++03 §9.5/1)。如果你熟悉这种语言,那么避免未定义或实现定义的行为并不特别困难。 - James McNellis
13
我对这段代码还有一个小问题。尽管函数名字叫做ntohl(),但它的行为和ntohl()完全不同。如果字节序已经正确,它就应该什么都不做。但是,在这里它无论如何都会交换字节顺序。 - kriss
5
@Tomek: 所以你更喜欢引发 UB 的解决方案而不是一个完全合法的解决方案?这完全没有意义。你说 "代码是合法的,但并不意味着它能工作",然后当谈到使用 union 时... "(AFAIR 它是可怕的 UB),但它看起来似乎可以工作。"我认为我从未听过一个更不一致的论点。你如何说:"如果你想在这么低的级别上操作位,迟早会遇到 UB 或 IDB"?你在说什么?那完全不正确,你只需要知道你在做什么。 - Ed S.
显示剩余6条评论

12

这是一个可以反转任何类型字节顺序的函数。

template <typename T>
T bswap(T val) {
    T retVal;
    char *pVal = (char*) &val;
    char *pRetVal = (char*)&retVal;
    int size = sizeof(T);
    for(int i=0; i<size; i++) {
        pRetVal[size-1-i] = pVal[i];
    }

    return retVal;
}

9

我很久以前找到了类似的东西。虽然可以用来逗笑,但食用需谨慎。我甚至没有编译它:

void * endian_swap(void * arg)
{
    unsigned int n = *((int*)arg);
    n = ((n >>  8) & 0x00ff00ff) | ((n <<  8) & 0xff00ff00);
    n = ((n >> 16) & 0x0000ffff) | ((n << 16) & 0xffff0000);
    *arg = n;   

    return arg;
}

呵呵。知道这会被踩。只是为了好笑才发的。很多年前我在EA面试时,他们有一个版本可以在1、2、4、8和16位宽度上进行移位操作,并问我它的作用是什么。 - Sniggerfardimungus
首先,我认为这只是一点娱乐而已。其次,我不确定别名规则是否适用。虽然arg可能指向将由n拥有的内存,但优化器不会发现它很重要,因为在分配给arg后它再也没有使用n了。别名规则直到你做像a=5;b=7;c=a+(b);这样的事情时才会生效,其中c的值无法从a的缓存值计算出来,因为对b的赋值可能会影响它。虽然我应该说((int*)arg) = n; =] - Sniggerfardimungus
2
如果性能很重要的话,这个版本会比那个做字节重排的版本更受欢迎。一旦你将值作为 dword 读回来,字节重排就会出现部分停顿。在这种情况下,你始终保持在 dword 大小上,并且它可以很好地进行流水线处理,没有停顿。我不惊讶 EA 展示了它,因为游戏开发人员在时间关键代码中使用这样的函数。 - Suma
话虽如此,我可能更喜欢将四个移位操作合并为一个更易读的版本。然而,在某些平台上,高移位计数的性能较差(如 PPC),因此他们所拥有的可能表现更好。 - Suma
2
*arg = n;?在没有进行强制类型转换的情况下,将值赋给已解引用的void指针是否有效?我预计编译器会报错。 - blubberdiblub
如果你的编译器(@blubberdiblub)报错了,你可以消除警告/错误。我不关心它所指向的类型 - 实际上,我正在明确地绕过它 - 只关心它实际指向的内容。 - Sniggerfardimungus

8
使用联合体是一种优雅的字节交换方法:
float big2little (float f)
{
    union
    {
        float f;
        char b[4];
    } src, dst;

    src.f = f;
    dst.b[3] = src.b[0];
    dst.b[2] = src.b[1];
    dst.b[1] = src.b[2];
    dst.b[0] = src.b[3];
    return dst.f;
}

根据jjmerelo的建议编写循环,更通用的解决方案可能是:

typedef float number_t;
#define NUMBER_SIZE sizeof(number_t)

number_t big2little (number_t n)
{
    union
    {
        number_t n;
        char b[NUMBER_SIZE];
    } src, dst;

    src.n = n;
    for (size_t i=0; i<NUMBER_SIZE; i++)
        dst.b[i] = src.b[NUMBER_SIZE-1 - i];

    return dst.n;
}

1
你不应该将赋值 dst.b <-> src.b 放入循环中吗? - jjmerelo
确实,编写循环是一种可能的解决方案。起初我这样做是为了使代码更具扩展性,以适用于其他大小(例如,转换双精度)。而且编译器在优化时会通过生成与没有循环相同的代码来序列化循环。将这四个赋值语句放在没有循环的情况下是为了简单起见,因为这样可能更容易理解。使用循环的解决方案如下:for (unsigned i=0; i<sizeof(ffoat); i++) dst.b[i] = src.b[sizeof(ffoat)-1 - i]; - Antonio Cañas Vargas
5
这是C++中的未定义行为。读取一个在联合体中没有被赋值的变量是无效的。在大多数编译器上可能会运行良好,但这就像不看路横穿马路一样危险。 - UKMonkey
1
非常感谢您的评论。在使用C语言编程30年后,我学到了有关联合的这个方面。当阅读您的评论时,我认为这是C++引入的一个特性,但我查找了Kernighan-Ritchie的第一版,它已经存在了:如果您最后写入的成员是联合中的最后一个,则安全读取该成员,否则行为未定义。 - Antonio Cañas Vargas
颠倒浮点数的字节并不一定会产生另一个有效的浮点数。 - user1050755
请参阅https://dev59.com/9mgu5IYBdhLWcg3wYF-3,了解有关C++中未定义行为的讨论。 - MarcusS

3

不要直接将数据复制到float类型中。保持其为char数据,交换字节,然后将其视为float。


2

SDL_endian.h 中稍作修改:

std::uint32_t Swap32(std::uint32_t x)
{
    return static_cast<std::uint32_t>((x << 24) | ((x << 8) & 0x00FF0000) |
                                      ((x >> 8) & 0x0000FF00) | (x >> 24));
}

float SwapFloat(float x)
{
    union
    {
        float f;
        std::uint32_t ui32;
    } swapper;
    swapper.f = x;
    swapper.ui32 = Swap32(swapper.ui32);
    return swapper.f;
}

1
颠倒浮点数的字节不一定会产生另一个有效的浮点数。 - user1050755

1

使用ntoa和相关函数从网络转换为主机以及从主机转换为网络可能会更容易...优点是它具有可移植性。这里是一篇文章的链接,解释了如何进行此操作。


0
这个值是一个浮点数,但是当我将该值“memcpy”到win32浮点类型中,然后在该值上调用_byteswap_ulong时,我总是得到0.0000?
这应该可以工作。你能发一下你的代码吗?
然而,如果你关心性能(也许你不关心,在这种情况下你可以忽略剩下的部分),应该可以避免使用memcpy,直接将其加载到目标位置并在那里交换字节,或者使用一个同时进行复制和交换的交换。

0

@morteza@AnotherParker已经提到了Boost库,指出对float的支持已被删除。但是,自从他们发表评论以来,它已经在库的子集中重新添加了。

使用Boost.Endian转换函数,版本1.77.0,就像我写这篇答案一样,您可以执行以下操作:

float input = /* some value */;
float reversed = input;
boost::endian::endian_reverse_inplace(reversed);

查看常见问题解答以了解为什么支持被删除,然后部分添加回来(主要是因为反转的float可能不再有效),并在此处查看支持历史记录。


我仍然不理解在给定字节序的IEEE 754 float如何在被具有相反字节序的系统交换其字节后变成NaN。常见问题解答中提到,即使整数和FP具有相同的字节序,这种情况也可能发生。但是,如果没有示例,我不会相信他们的说法。 - cesss
@cesss 这可能与系统整数字节序无关。事实上,这是IEEE 754定义的固有特性。NaN被定义为指数填充了“1”和非零尾数的数字,在进行字节反转后可以在实践中获得。例如,在我的amd64系统(小端)上,四个字节7f ffffffa0 0 0被解释为5.75752e-41,但反转会给我一个信号NaN。 - vvanpelt
显然,如果你在主机系统中正确的字节序下进行了浮点数的字节交换,然后你尝试在同一台主机上读取它,得到的结果是垃圾。但这不是重点。问题在于FAQ声称,如果你在具有与第一个主机相反字节序的主机上读取这样一个字节交换的浮点数,你可以得到NaN。据我所知,这是不可能的,因为第二个主机以与第一个主机本地字节序完全相同的方式读取浮点数(当然,第二个主机读取它时进行了字节交换,但它也具有相反的字节序,所以它看到的是相同的)。 - cesss
1
@cesss,我认为你是在FAQ中加入了它本来没有的内容。例如,他们从未谈到过在不同字节序系统之间进行传输的问题。我认为这里的主要问题是,在浮点数的字节序转换过程中,可能会出现无效的瞬态值,而整数则不会有这个问题,因此用户可能会经历由此产生的副作用。 - vvanpelt
啊,我想这解决了,谢谢!我从来没有将已交换的float存储为float类型,所以我没想到这一点。当他们提到代码可能会生成NaN时,我认为他们指的是在传输结束时而不是在中途出现。谢谢! - cesss

0

在某些情况下,特别是在Modbus上:浮点数的网络字节顺序为:

nfloat[0] = float[1]
nfloat[1] = float[0]
nfloat[2] = float[3]
nfloat[3] = float[2]

反转浮点数的字节不一定会产生另一个有效的浮点数。 - user1050755

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