将 2 个字节转换为有符号的 16 位整数的正确方法是什么?

chq*_*lie 33 c casting language-lawyer

这个回答中zwol提出了这个主张:

将来自外部源的两个字节数据转换为 16 位有符号整数的正确方法是使用如下辅助函数:

#include <stdint.h>

int16_t be16_to_cpu_signed(const uint8_t data[static 2]) {
    uint32_t val = (((uint32_t)data[0]) << 8) | 
                   (((uint32_t)data[1]) << 0);
    return ((int32_t) val) - 0x10000u;
}

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    uint32_t val = (((uint32_t)data[0]) << 0) | 
                   (((uint32_t)data[1]) << 8);
    return ((int32_t) val) - 0x10000u;
}
Run Code Online (Sandbox Code Playgroud)

上述哪个函数合适取决于数组是包含小端还是大端表示。字节序是不是问题的问题,在这里,我很奇怪,为什么zwol减去0x10000uuint32_t值转换为int32_t

为什么这是正确的方法

转换为返回类型时如何避免实现定义的行为?

既然您可以假设 2 的补码表示,那么这个更简单的转换将如何失败: return (uint16_t)val;

这个幼稚的解决方案有什么问题:

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}
Run Code Online (Sandbox Code Playgroud)

M.M*_*M.M 20

如果int是 16 位,那么如果return语句中表达式的值超出范围,则您的版本依赖于实现定义的行为int16_t

但是第一个版本也有类似的问题;例如,如果int32_t是 typedef for int,并且输入字节都是0xFF,则 return 语句中的减法结果是UINT_MAX在转换为 时导致实现定义的行为int16_t

恕我直言,您链接的答案有几个主要问题。


jpa*_*jpa 8

这应该是迂腐正确的,并且也适用于使用符号位1 的补码表示的平台,而不是通常的2 的补码。假设输入字节为 2 的补码。

int le16_to_cpu_signed(const uint8_t data[static 2]) {
    unsigned value = data[0] | ((unsigned)data[1] << 8);
    if (value & 0x8000)
        return -(int)(~value) - 1;
    else
        return value;
}
Run Code Online (Sandbox Code Playgroud)

由于分支的原因,它会比其他选项更贵。

这样做的目的是避免任何关于int表示如何unsigned与平台上的表示相关的假设。int需要强制转换以保留适合目标类型的任何数字的算术值。由于反转确保 16 位数字的最高位为零,因此该值将适合。然后一元-和 1 的减法应用 2 的补码否定的通常规则。根据平台,INT16_MIN如果它不适合int目标上的类型,仍然可能溢出,在这种情况下long应该使用。

问题中与原始版本的区别在于返回时间。虽然原始总是减去0x10000并且 2 的补码让有符号溢出将其包装到int16_t范围内,但此版本具有if避免有符号包装(未定义)的显式。

现在在实践中,当今使用的几乎所有平台都使用 2 的补码表示。事实上,如果平台有符合标准的stdint.h定义int32_t,它必须使用 2 的补码。这种方法有时派上用场的是一些根本没有整数数据类型的脚本语言 - 您可以修改上面显示的浮点数操作,它会给出正确的结果。


Max*_*kin 6

算术运算符shiftbitwise-or in expression(uint16_t)data[0] | ((uint16_t)data[1] << 8)不适用于小于 的类型int,因此这些uint16_t值被提升为int(or unsignedif sizeof(uint16_t) == sizeof(int))。尽管如此,这应该会产生正确的答案,因为只有较低的 2 个字节包含该值。

big-endian 到 little-endian 转换的另一个迂腐正确的版本(假设 little-endian CPU)是:

#include <string.h>
#include <stdint.h>

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    memcpy(&r, data, sizeof r);
    return __builtin_bswap16(r);
}
Run Code Online (Sandbox Code Playgroud)

memcpy用于复制 的表示,int16_t这是符合标准的方法。这个版本也编译成1条指令movbe,见汇编

  • @MM:我认为 Maxim 是在说“在当前的编译器中*实际上*不能”。当然,编译器不可能一次性就识别出将连续字节加载到整数中。在 GCC3 几十年前放弃它之后,GCC7 或 8 最终在不需要字节反转的情况下重新引入了加载/存储合并。但一般来说,编译器在实践中往往需要帮助来处理 CPU 可以有效完成但 ISO C 忽略/拒绝可移植公开的许多事情。可移植 ISO C 并不是一种高效代码位/字节操作的好语言。 (3认同)

i48*_*486 6

另一种方法 - 使用union

union B2I16
{
   int16_t i;
   byte    b[2];
};
Run Code Online (Sandbox Code Playgroud)

在节目中:

...
B2I16 conv;

conv.b[0] = first_byte;
conv.b[1] = second_byte;
int16_t result = conv.i;
Run Code Online (Sandbox Code Playgroud)

first_byte并且second_byte可以根据小端或大端模型进行交换。这种方法不是更好,而是替代方法之一。

  • 联合类型不是双关语[未指定行为](https://en.wikipedia.org/wiki/Type_punning#Use_of_union)吗? (2认同)
  • @MaximEgorushkin:维基百科不是解释 C 标准的权威来源。 (2认同)
  • @EricPostpischil 专注于信使而不是消息是不明智的。 (2认同)