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


31

这个答案中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;
}

上面的哪个函数合适,取决于数组包含小端还是大端表示。字节不是这里要讨论的问题,我想知道为什么zwol0x10000uuint32_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);
}

强制转换为时的确切行为int16_t是实现定义的,因此幼稚的方法不可移植。
nwellnhof

@nwellnhof没有任何演员int16_t
MM

不指定要使用的映射就无法回答标题中的问题
MM

4
两种方法都依赖于实现定义的行为(将无符号值转换为不能表示该值的带符号类型)。例如。在第一种方法中,0xFFFF0001u不能表示为int16_t,在第二种方法0xFFFFu中,不能表示为int16_t
桑德·戴克

1
“因为可以假设2的补码表示形式” [需要引用]。C89和C99当然不否认补码和符号幅度表示。Qv,stackoverflow.com
Eric Towers

Answers:


20

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

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

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


2
但是正确的方法是什么?
idmean

@idmean需要先澄清问题,然后才能回答,我已在问题下的评论中要求提供,但OP尚未答复
MM

1
@MM:我编辑了问题,指定字节序不是问题。恕我直言,zwol试图解决的问题是转换为目标类型时实现定义的行为,但是我同意你的观点:我相信他是错误的,因为他的方法还有其他问题。您将如何有效地解决实施定义的行为?
chqrlie

@chqrlieforyellowblockquotes我不是专门指字节序。您是否只想将两个输入八位位组的确切位放入int16_t?中?
MM

@MM:是的,这就是问题所在。我写了字节,但是正确的字确实应该是八位字节,因为类型是uchar8_t
chqrlie

7

这在理论上应该是正确的,并且也可以在使用符号位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的补码表示形式。实际上,如果平台具有stdint.h定义的标准兼容int32_t,则它必须使用2的补码。有些脚本语言根本没有整数数据类型,这种方法有时很方便-您可以修改上面显示的针对float的操作,它将给出正确的结果。


C标准明确规定,int16_t任何intxx_t及其无符号变体必须使用2的补码表示而没有填充位。托管这些类型并为使用另一个表示形式将需要有目的的反常架构int,但我想DS9K可以通过这种方式进行配置。
chqrlie

@chqrlieforyellowblockquotes好点了,我改用int以避免混淆。确实,如果平台定义,int32_t则必须为2的补码。
jpa

这些类型在C99中通过以下方式进行了标准化:C99 7.18.1.1精确宽度整数类型 typedef名称intN_t 指定一个带符号整数类型,其宽度为width N,没有填充位,并且为二进制补码。因此,int8_t表示宽度正好为8位的有符号整数类型。该标准仍支持其他表示形式,但适用于其他整数类型。
chqrlie

对于更新的版本,(int)value如果type int只有16位,则具有实现定义的行为。恐怕您需要使用(long)value - 0x10000,但是在非2的补码体系结构上,该值0x8000 - 0x10000不能表示为16位int,因此问题仍然存在。
chqrlie

@chqrlieforyellowblockquotes是的,只是注意到了相同的问题,我改用〜进行了修复,但long同样可以很好地工作。
jpa

6

另一种方法-使用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_bytesecond_byte可以根据大端或小端模式进行交换。此方法不是更好,但是是替代方法之一。


2
联合体类型不是在修饰未指定的行为吗?
Maxim Egorushkin

1
@MaximEgorushkin:维基百科不是解释C标准的权威来源。
Eric Postpischil

2
@EricPostpischil专注于使者而不是消息是不明智的。
Maxim Egorushkin

1
@MaximEgorushkin:哦,是的,哎呀,我看错了你的评论。假设byte[2]int16_t具有相同的大小,则它是两种可能的排序中的一种或另一种,而不是一些任意的按位排序的随机值。因此,您至少可以在编译时检测实现具有什么字节序。
Peter Cordes

1
该标准明确指出,联合成员的值是将成员中存储的位解释为该类型的值表示的结果。在实现中有一些实现定义的方面,类型的表示是实现定义的。
MM

6

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

从大尾数到小尾数的转换(假设小尾数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);
}

memcpy用于复制的表示,int16_t这是符合标准的方式。该版本还编译为1条指令movbe,请参见汇编


1
@MM __builtin_bswap16存在一个原因是因为ISO C中的字节交换无法高效实现。
Maxim Egorushkin

1
不对; 编译器可以检测到代码实现了字节交换,并将其转换为高效的内置函数
MM

1
转换int16_tuint16_t定义明确:负值转换为大于的值INT_MAX,但将这些值转换回uint16_t实现定义的行为:6.3.1.3有符号和无符号整数 1.如果将整数类型的值转换为_Bool以外的其他整数类型,则该值可以用新类型表示,但不变。... 3.否则,将对新类型进行签名,并且无法在其中表示值;结果是实现定义的,还是引发实现定义的信号。
chqrlie

1
@MaximEgorushkin gcc在16位版本中似乎做得不太好,但是clang为ntohs/ __builtin_bswap|/ <<模式生成相同的代码:gcc.godbolt.org/z/rJ-j87
PSkocik

3
@MM:我认为Maxim是在说“ 目前的编译器无法实现 ”。当然,编译器无法一次吸取并识别将连续字节加载为整数。GCC7或8做最后再推出加载/存储合并为这里字节反转的情况下没有必要,以后GCC3几十年前放弃了它。但是总的来说,编译器在实践中往往需要帮助,CPU可以有效地完成很多工作,而ISO C却忽略了/拒绝将这些工作暴露出来。可移植的ISO C并不是有效的代码位/字节操作的好语言。
Peter Cordes

4

这是另一个仅依赖于可移植行为和定义良好的行为的版本(标头#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版本用编译为单movbe条指令clanggcc版本次优,请参见汇编


@chqrlieforyellowblockquotes您的主要关注似乎已经uint16_tint16_t转换,这个版本不具有转换,所以在这里你去。
Maxim Egorushkin

2

我要感谢所有贡献者的回答。这是集体工作归结为:

  1. 根据C标准7.20.1.1精确宽度整数类型:types uint8_tint16_t并且uint16_t必须使用二进制补码表示形式,而没有任何填充位,因此表示形式的实际位明确地是数组中2个字节的那些,按指定的顺序函数名称。
  2. 使用(unsigned)data[0] | ((unsigned)data[1] << 8)(对于Little Endian版本)计算无符号的16位值将编译为一条指令,并产生无符号的16位值。
  3. 根据C标准6.3.1.3有符号和无符号整数:如果uint16_t类型int16_t的值不在目标类型的范围内,则将类型的值转换为有符号的类型具有实现定义的行为。对于其表示形式已精确定义的类型,没有特殊规定。
  4. 为避免此实现定义的行为,可以测试无符号值是否大于,INT_MAX并通过减去来计算相应的有符号值0x10000。按照zwol的建议对所有值执行此操作可能会产生int16_t具有相同实现定义的行为的范围之外的值。
  5. 测试该0x8000位会明显导致编译器生成效率低下的代码。
  6. 在没有实现定义行为的情况下进行更有效的转换时,将通过联合使用类型修剪,但是有关此方法的定义性的争论仍然是开放的,即使是在C标准的委员会级别上也是如此。
  7. 可以轻便地执行类型修剪,并使用定义行为memcpy

结合点2和点7,这是一个可移植且完全定义的解决方案,可以使用gccclang高效地编译为一条指令:

#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;
}

64位汇编

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

我不是语言律师,但是只有char类型可以别名或包含任何其他类型的对象表示形式。uint16_t是没有一个char类型,因此memcpyuint16_tint16_t是不明确定义的行为。该标准只需要进行char[sizeof(T)] -> T > char[sizeof(T)]转换memcpy即可,并且定义明确。
Maxim Egorushkin

memcpy的充其量最多uint16_t只能int16_t由实现定义,而不是可移植的,定义不明确的,就像将一个分配给另一个完全一样,并且您不能使用来神奇地规避它memcpy。无论是否uint16_t使用二进制补码表示或是否存在填充位都没有关系-这不是C标准定义或要求的行为。
Maxim Egorushkin

说了这么多话,您的“解决方案”可以归结为r = umemcpy(&r, &u, sizeof u)但后者并不比前者好,是吗?
Maxim Egorushkin
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.