[英]What is the correct way to convert 2 bytes to a signed 16-bit integer?
将来自外部源的两个字节数据转换为 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;
}
上述哪个函数合适取决于数组是包含小端还是大端表示。 字节序不是这里的问题,我想知道为什么0x10000u
从转换为int32_t
的uint32_t
值中减去0x10000u
。
为什么这是正确的方法?
转换为返回类型时如何避免实现定义的行为?
既然您可以假设 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);
}
如果int
是 16 位,那么如果return
语句中的表达式值超出int16_t
的范围,则您的版本依赖于实现定义的行为。
但是第一个版本也有类似的问题; 例如,如果int32_t
是int
的 typedef,并且输入字节都是0xFF
,则 return 语句中的减法结果是UINT_MAX
,当转换为int16_t
时会导致实现定义的行为。
恕我直言,您链接的答案有几个主要问题。
这应该是迂腐正确的,并且也适用于使用符号位或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;
}
由于分支的原因,它会比其他选项更贵。
这样做的目的是避免任何关于int
表示如何与平台上的unsigned
表示相关的假设。 需要转换为int
以保留适合目标类型的任何数字的算术值。 由于反转确保 16 位数字的最高位为零,因此该值将适合。 然后一元-
和 1 的减法应用 2 的补码否定的通常规则。 根据平台的不同,如果INT16_MIN
不适合目标上的int
类型,它仍可能溢出,在这种情况下应使用long
。
问题中与原始版本的区别在于返回时间。 虽然原始总是减去0x10000
和 2 的补码让有符号溢出将其包装到int16_t
范围,但此版本具有明确的if
避免有符号包装( 未定义)。
现在在实践中,当今使用的几乎所有平台都使用 2 的补码表示。 事实上,如果平台具有定义int32_t
的符合标准的stdint.h
,则它必须使用 2 的补码。 这种方法有时派上用场的是一些根本没有整数数据类型的脚本语言 - 您可以修改上面显示的浮点数操作,它会给出正确的结果。
表达式(uint16_t)data[0] | ((uint16_t)data[1] << 8)
的算术运算符移位和按位或 (uint16_t)data[0] | ((uint16_t)data[1] << 8)
不适用于小于int
类型,因此这些uint16_t
值被提升为int
(或unsigned
if 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);
}
另一种方法 - 使用union
:
union B2I16
{
int16_t i;
byte b[2];
};
在节目中:
...
B2I16 conv;
conv.b[0] = first_byte;
conv.b[1] = second_byte;
int16_t result = conv.i;
first_byte
和second_byte
可以根据小端或大端模型交换。 这种方法不是更好,而是替代方法之一。
这是另一个仅依赖于可移植和明确定义的行为的版本(标头#include <endian.h>
不是标准的,代码是):
#include <endian.h>
#include <stdint.h>
#include <string.h>
static inline void swap(uint8_t* a, uint8_t* b) {
uint8_t t = *a;
*a = *b;
*b = t;
}
static inline void reverse(uint8_t* data, int data_len) {
for(int i = 0, j = data_len / 2; i < j; ++i)
swap(data + i, data + data_len - 1 - i);
}
int16_t be16_to_cpu_signed(const uint8_t data[2]) {
int16_t r;
#if __BYTE_ORDER == __LITTLE_ENDIAN
uint8_t data2[sizeof r];
memcpy(data2, data, sizeof data2);
reverse(data2, sizeof data2);
memcpy(&r, data2, sizeof r);
#else
memcpy(&r, data, sizeof r);
#endif
return r;
}
little-endian 版本使用clang
编译为单个movbe
指令, gcc
版本不太理想,请参阅assembly 。
我要感谢所有贡献者的回答。 以下是集体作品的内容:
uint8_t
、 int16_t
和uint16_t
必须使用没有任何填充位的二进制补码表示,因此表示的实际位是数组中 2 个字节的明确位,在由函数名指定的顺序。(unsigned)data[0] | ((unsigned)data[1] << 8)
计算无符号的 16 位值(unsigned)data[0] | ((unsigned)data[1] << 8)
(unsigned)data[0] | ((unsigned)data[1] << 8)
(对于小端版本)编译为一条指令并产生一个无符号的 16 位值。uint16_t
类型的值转换为有符号类型int16_t
具有实现定义的行为。 对于精确定义表示的类型没有特别规定。INT_MAX
并通过减去0x10000
来计算相应的有符号值。 按照zwol 的建议对所有值执行此操作可能会产生具有相同实现定义行为的int16_t
范围之外的值。0x8000
位的测试显式导致编译器生成低效代码。memcpy
可移植地执行类型双关并定义行为。结合第 2 点和第 7 点,这是一个可移植且完全定义的解决方案,它可以使用gcc和clang有效地编译为单个指令:
#include <stdint.h>
#include <string.h>
int16_t be16_to_cpu_signed(const uint8_t data[2]) {
int16_t r;
uint16_t u = (unsigned)data[1] | ((unsigned)data[0] << 8);
memcpy(&r, &u, sizeof r);
return r;
}
int16_t le16_to_cpu_signed(const uint8_t data[2]) {
int16_t r;
uint16_t u = (unsigned)data[0] | ((unsigned)data[1] << 8);
memcpy(&r, &u, sizeof r);
return r;
}
be16_to_cpu_signed(unsigned char const*):
movbe ax, WORD PTR [rdi]
ret
le16_to_cpu_signed(unsigned char const*):
movzx eax, WORD PTR [rdi]
ret
为什么不直接使用您的“天真的解决方案”,而是将每个元素转换为int16_t
而不是uint16_t
?
int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
return (int16_t)data[0] | ((int16_t)data[1] << 8);
}
那么你就不必处理将无符号整数转换为有符号整数(并且可能超出有符号整数范围)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.