将无符号整数转换为有符号整数 C

27

我想将65529unsigned int转换为带符号的int。 我尝试过这样的强制转换:

unsigned int x = 65529;
int y = (int) x;

但是当它应该返回-7时,y仍然返回65529。为什么会这样?


10
32位元时代已经到来了几十年。如果你仍在使用古老的编译器来编译DOS或嵌入式系统,其中int只有16位元,那么结果可能会如你所料。 - phuclv
为什么会是-7?65529 - 65535 = -6 ... - MarcusJ
3
因为2的16次方等于65536,而不是65535(提示:2的幂次方始终是偶数)。因此,16位整数的正确计算方式为65529-65536,预期结果为-7。您可以从负数开始倒数:-1是65535,-2是65534,...,-7是65529。但显然OP没有使用16位整数。 - Tom Karzes
2
@MarcusJ 哈哈,是的,我看到了,但我偶然发现了这个页面,因为没有人回复你的评论(而且你也没有删除它),所以我想迟到总比不到好。 - Tom Karzes
为什么会是“-7”呢?通常情况下,int类型的宽度为32位,所以65539远远没有超出它的范围。 - undefined
显示剩余3条评论
9个回答

39

看起来你期望 intunsigned int 是16位整数,但实际上它们通常是32位整数——足够大以避免你期望的溢出。

请注意,没有一种完全符合C标准的方法可以做到这一点,因为将超出范围的有符号/无符号值进行强制转换是由具体实现定义的。但在大多数情况下,以下方法仍然有效:

unsigned int x = 65529;
int y = (short) x;      //  If short is a 16-bit integer.
或者,另外一种选择是:
unsigned int x = 65529;
int y = (int16_t) x;    //  This is defined in <stdint.h>

2
如果你能保证short是一个16位整数,那么int y = (short)x;就可以工作。或者,如果你的编译器有<stdint.h>头文件,你也可以使用int16_t - Mysticial
@Nayefc 正如我所说,您可以通过打印 sizeof(int)sizeof(short) 或其他需要的类型来检查其大小。 - Szabolcs
3
有一个基本符合 C 语言标准的方法来实现它:y = x < 32767 ? (int)x : (x > 32768 ? -(int)-x : -32768);(唯一的问题在于 -32768 不一定可以表示为有符号整数,所以你需要决定如何处理它)。 - caf
是的,那也可以——虽然这是一种更加粗糙的方法。我没有考虑到要一直分支来做这件事…… - Mysticial
只是出于好奇,我刚刚在启用x64的VS2010中进行了全面优化的尝试。你是对的,它不会分支。正如我半预期的那样,它编译成了一堆“cmp”和“cmov”指令。所以它足够聪明,可以摆脱分支,但不足以将其转换为单个“movsx”符号扩展。编辑:即使将第一个“32767”更改为“32768”。 - Mysticial
显示剩余2条评论

6

我知道这是一个老问题,但它是一个好问题,那么怎么样呢?

unsigned short int x = 65529U;
short int y = *(short int*)&x;

printf("%d\n", y);

这样做的原因是我们将 x 的地址强制转换为其类型的带符号版本,这是 C 标准允许的。并非所有此类类型重解释(实际上大部分)都是合法的。标准规定如下。

对象的存储值只能由以下类型之一的 lvalue 访问:

  • 对象的声明类型,
  • 对象的声明类型的限定版本,
  • 与对象的声明类型相对应的有符号或无符号类型,
  • 与对象的声明类型的限定版本相对应的有符号或无符号类型,
  • 包括前述类型之一在其成员中的聚合或联合类型(包括递归地作为子聚合或包含联合的成员),
  • 字符类型。
因此,遗憾的是,由于我们正在像访问带符号类型一样访问 x 的位(通过指针),所以实际的转换操作被替换为仅读取一个负带符号短整数,并且转换顺利进行。但是,在补码机器上可能会出现问题,但是这些机器非常非常罕见和过时,我甚至不会费心找它们。

@pm89 我不知道你对“工作”的定义是什么,但至少编译器实现可以针对使用 1 补码而非 2 补码整数的系统。对于这样的系统,该解决方案会“工作”,但结果会有“不同的值”。另一方面,编译器可以将 short 定义为 32 位大小。在这种情况下,转换后的值将保持为正数。当然,如果你使用今天成熟的编译器和标准的 x86/x64 硬件,这些都不相关。但就算讨论“不考虑编译器实现”,也请不要无知地忽略标准。 - grek40
1
@pm89 接受的答案明确说明了实现定义的行为,因此它具有相同的理论问题,但它并不声称为标准的任何实现产生相同的结果。 - grek40

5

@Mysticial明白了。一个short通常是16位,下面将用示例来说明:

int main()  
{
    unsigned int x = 65529;
    int y = (int) x;
    printf("%d\n", y);

    unsigned short z = 65529;
    short zz = (short)z;
    printf("%d\n", zz);
}

65529
-7
Press any key to continue . . .


稍微详细一点,这与有符号整数在内存中的存储方式有关。搜索二进制补码表示法以了解更多细节,但以下是基础知识。

现在让我们看看65529十进制数。它可以用十六进制表示为FFF9h。我们也可以将其表示为二进制:

11111111 11111001

当我们声明short zz = 65529;时,编译器将65529解释为有符号值。在二进制补码表示法中,最高位表示有符号值是正数还是负数。在这种情况下,您可以看到最高位是1,因此它被视为负数。这就是为什么它打印出-7的原因。

对于unsigned short,我们不关心符号,因为它是unsigned。因此,当我们使用%d打印它时,我们使用所有16位,因此它被解释为65529


2
为了理解这一点,您需要知道CPU使用二进制补码(可能不是全部,但很多)来表示有符号数字。
    byte n = 1; //0000 0001 =  1
    n = ~n + 1; //1111 1110 + 0000 0001 = 1111 1111 = -1

此外,类型int和unsigned int的大小可能取决于您的CPU。在执行类似以下的特定操作时:

   #include <stdint.h>
   int8_t ibyte;
   uint8_t ubyte;
   int16_t iword;
   //......

1

对于16位整数,值65529u和-7的表示是相同的。只有位的解释不同。

对于更大的整数和这些值,您需要进行符号扩展;一种方法是使用逻辑操作。

int y = (int )(x | 0xffff0000u); // assumes 16 to 32 extension, x is > 32767

如果速度不是问题,或者你的处理器分割速度很快,
int y = ((int ) (x * 65536u)) / 65536;

乘法将左移16位(假设进行了16到32的扩展),而除法将向右移动并保持符号。


这仅适用于小端两补系统。 - yyny
1
问题者的机器是按照二进制补码来表示的,这可以从他报告的数值中看出。字节序与此无关;我只使用int值,而不是内存中的单个字节。 - Doug Currie

0
你期望你的 int 类型是16位宽度,这种情况下你确实会得到一个负值。但很可能它是32位宽度,因此有符号的 int 可以很好地表示65529。你可以通过打印 sizeof(int) 来检查这一点。

0
回答上面评论中提出的问题 - 可以尝试这样做:
unsigned short int x = 65529U;
short int y = (short int)x;

printf("%d\n", y);

或者

unsigned short int x = 65529U;
short int y = 0;

memcpy(&y, &x, sizeof(short int);
printf("%d\n", y);

0

我知道这是一个旧问题,但我认为回答者可能误解了它。我认为原意是将接收到的16位比特序列作为无符号整数(技术上为unsigned short)转换为有符号整数。当你需要将从网络接收的数据从网络字节顺序转换为主机字节顺序时,可能会发生这种情况(最近就发生在我身上)。在这种情况下,请使用联合:

unsigned short value_from_network;
unsigned short host_val = ntohs(value_from_network);
// Now suppose host_val is 65529.
union SignedUnsigned {
  short          s_int;
  unsigned short us_int;
};
SignedUnsigned su;
su.us_int = host_val;
short minus_seven = su.s_int;

现在,minus_seven 的值为-7。

1
OP并没有明确要求16位,只是从“unsigned”到“signed”的转换未如预期般进行。除此之外,在有符号类型中存储超出范围的无符号值始终是实现定义的行为,“short”和“unsigned short”不一定需要具有16位宽度;它们要求至少具有16位宽度。如果您需要准确的宽度,请使用精确宽度类型,例如“int16_t”或“uint16_t”。请注意,这些精确宽度类型是可选类型,但它们仍然受到常见平台的支持。 - ad absurdum
一个 short 不能保证是16位(尽管在大多数平台上它可能是)。如果你想要16位,为什么不使用标准类型 uint16_tint16_t,它们保证是16位?即使 ntohs() 的返回类型是 uint16_t,也是有充分理由的。联合体是一个不错的技巧,但似乎过于复杂了。从 uint16_tint16_t 的强制转换将具有相同的效果,并且更加描述性(根据编译器和编译器选项,可能更有效率)。 - wovano

0

由于使用无符号值来表示正数,因此将其转换可以通过将最高位设置为0来完成。因此,程序不会将其解释为二进制补码值。但需要注意的是,这将导致在接近无符号类型的最大值时丢失信息。

template <typename TUnsigned, typename TSinged>
TSinged UnsignedToSigned(TUnsigned val)
{
    return val & ~(1 << ((sizeof(TUnsigned) * 8) - 1));
}

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