chq*_*lie 33 c casting language-lawyer
将来自外部源的两个字节数据转换为 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减去0x10000u
从uint32_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
。
恕我直言,您链接的答案有几个主要问题。
这应该是迂腐正确的,并且也适用于使用符号位或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 的补码。这种方法有时派上用场的是一些根本没有整数数据类型的脚本语言 - 您可以修改上面显示的浮点数操作,它会给出正确的结果。
算术运算符shift和bitwise-or in expression(uint16_t)data[0] | ((uint16_t)data[1] << 8)
不适用于小于 的类型int
,因此这些uint16_t
值被提升为int
(or 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);
}
Run Code Online (Sandbox Code Playgroud)
memcpy
用于复制 的表示,int16_t
这是符合标准的方法。这个版本也编译成1条指令movbe
,见汇编。
另一种方法 - 使用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
可以根据小端或大端模型进行交换。这种方法不是更好,而是替代方法之一。