C语言中的有符号转无符号转换 - 是否总是安全的?

164

假设我有以下的C代码。

unsigned int u = 1234;
int i = -5678;

unsigned int result = u + i;

这里发生了什么隐式转换,这段代码对所有的ui 都安全吗?(在这个例子中,即使result会溢出为一些巨大的正数,我可以将其强制转换回一个int并获得真正的结果。)

8个回答

263

简短回答

通过添加 UINT_MAX + 1 将你的 i 转换为无符号整数,然后使用无符号值进行加法运算,导致一个大的 result(取决于 ui 的值)。

详细回答

根据 C99 标准:

6.3.1.8 Usual arithmetic conversions

  1. 如果两个操作数有相同的类型,则不需要进一步的转换。
  2. 否则,如果两个操作数都具有有符号整数类型或无符号整数类型,则具有较小整数转换秩的操作数将转换为具有更大秩的操作数的类型。
  3. 否则,如果具有无符号整数类型的操作数的秩大于或等于另一个操作数的类型的秩,则带有有符号整数类型的操作数将转换为具有无符号整数类型的操作数的类型。
  4. 否则,如果具有有符号整数类型的操作数的类型可以表示无符号整数类型的所有值,则具有无符号整数类型的操作数将转换为具有有符号整数类型的操作数的类型。
  5. 否则,两个操作数都将转换为与具有有符号整数类型的操作数的类型对应的无符号整数类型。

在你的情况下,我们有一个无符号整数(u)和有符号整数(i)。参考上述第三点,由于两个操作数具有相同的秩,因此您的 i 将需要被转换为无符号整数。

6.3.1.3 Signed and unsigned integers

  1. 当使用整数类型的值转换为除 _Bool 以外的另一种整数类型时,如果该值可以由新类型表示,则它不变。
  2. 否则,如果新类型是无符号的,则通过反复添加或减去新类型中可以表示的最大值加 1,直到该值在新类型的范围内。
  3. 否则,新类型为带符号类型,而该值无法在其中表示; 结果是实现定义的,或者触发实现定义的信号。

现在我们需要参考上述第二点。通过添加 UINT_MAX + 1 将您的 i 转换为无符号值。因此结果将取决于您的实现中如何定义 UINT_MAX。它会很大,但不会溢出,因为:

6.2.5 (9)

涉及无符号操作数的计算永远不可能溢出,因为不能由结果无法表示的结果的无符号整数类型将被减少模所得到的数字是大于可以表示的结果类型的最大值。

Bonus: Arithmetic Conversion Semi-WTF

#include <stdio.h>

int main(void)
{
  unsigned int plus_one = 1;
  int minus_one = -1;

  if(plus_one < minus_one)
    printf("1 < -1");
  else
    printf("boring");

  return 0;
}

您可以使用此链接在线尝试:https://repl.it/repls/QuickWhimsicalBytes

奖励:算术转换的副作用

可以利用算术转换规则通过将无符号值初始化为-1来获取UINT_MAX的值,即:

unsigned int umax = -1; // umax set to UINT_MAX

由于上述转换规则的存在,这保证了无论系统中使用的有符号数表示方式如何,此代码都可以移植。更多信息请参见此SO问题


我不明白为什么它不能像处理正数一样,先取绝对值再将其视为无符号数? - Jose Salvatierra
将有符号数转换为无符号数时,我们需要加上无符号值的最大值(UINT_MAX +1)。同样地,将无符号数转换为有符号数的简单方法是什么?我们需要从最大值(对于无符号字符为256)中减去给定的数字吗?例如:将140转换为有符号数变为-116。但20仍然是20。那么这里有什么简单的技巧吗? - Jon Wheelock
@JonWheelock 请参考:https://dev59.com/KWsy5IYBdhLWcg3wyxCi - Ozgur Ozcitak

26

将有符号数转换为无符号数并不一定只是复制或重新解释有符号值的表示。引用C标准(C99 6.3.1.3):

当具有整数类型的值被转换为除_Bool之外的另一种整数类型时,如果该值可以由新类型表示,则它保持不变。

否则,如果新类型是无符号的,则通过重复添加或减去比新类型中可以表示的最大值多一个的最大值,直到该值在新类型的范围内为止。

否则,新类型是有符号的,而该值无法在其中表示;结果要么是实现定义的,要么是引发实现定义的信号。

对于这些天几乎普遍使用的二进制补码表示,规则确实对应于重新解释位。但对于其他表示(符号-大小或反码),C实现仍必须安排相同的结果,这意味着转换不能只复制位。例如,(unsigned)-1 == UINT_MAX,无论表示如何。

总的来说,在C中,转换是定义为对值而不是表示进行操作。

回答原始问题:

unsigned int u = 1234;
int i = -5678;

unsigned int result = u + i;

i的值被转换为无符号整数,得到UINT_MAX + 1 - 5678。然后将此值加上无符号值1234,得到UINT_MAX + 1 - 4444

(与无符号溢出不同,有符号溢出会引发未定义行为。环绕是常见的,但不被C标准保证——编译器优化可能会对做出不必要假设的代码造成破坏。)


8

参考《C程序设计语言》第二版(ISBN 0131103628),

  • 加法操作会导致int类型转换成unsigned int类型。
  • 假设使用二进制补码表示且类型大小相等,则位模式不会改变。
  • 将unsigned int类型转换为signed int类型是依赖具体实现的。(但在大多数平台上,它可能以你期望的方式工作。)
  • 当合并大小不同的有符号和无符号类型时,规则会更加复杂。

4

在将有符号数转换为无符号数时,有两种可能性。原本是正数的数字保持(或解释为)相同的值。原本是负数的数字现在将被解释为更大的正数。


3

当一个无符号变量和一个有符号变量进行加法运算(或任何二进制操作)时,两者都会隐式转换为无符号,这在本例中将导致巨大的结果。

因此,安全性意味着结果可能会很大并且错误,但永远不会崩溃。


1
不是真的。 6.3.1.8 常规算术转换 如果您将int和unsigned char相加,后者将转换为int。如果您将两个unsigned char相加,它们将转换为int。 - 2501

2

这里发生了什么隐式转换,

i 将会被转换为无符号整数。

这段代码对于所有的 u 和 i 的值都是安全的吗?

从定义上来说是安全的(参见 https://dev59.com/QHVD5IYBdhLWcg3wNY1Z#50632)。

规则通常用难以理解的标准术语编写,但基本上无论有符号整数中使用了什么表示方式,无符号整数都将包含该数字的二进制补码表示。

加法、减法和乘法将在这些数字上正确地工作,产生另一个无符号整数,其中包含一个二进制补码数字,表示“真实结果”。

除法和强制转换为较大的无符号整数类型将具有明确定义的结果,但这些结果将不是“真实结果”的二进制补码表示。

(在这个例子中,即使结果溢出到某个巨大的正数,也可以将其转换回 int 并获得真实结果。)

虽然标准定义了从有符号数到无符号数的转换,但反向转换是由实现定义的。GCC和MSVC都定义了这种转换,使得当将存储在无符号整数中的二进制补码数字转换回有符号整数时,您将获得“真实结果”。我预计只有在不使用二进制补码表示有符号整数的晦涩系统上才会发现其他行为。

https://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html#Integers-implementation https://msdn.microsoft.com/en-us/library/0eex498h.aspx


1

正如之前所回答的,您可以在有符号和无符号之间自由转换而不会出现问题。 有符号整数的边界情况是-1(0xFFFFFFFF)。 尝试从中加减并且您会发现您可以进行转换并且结果是正确的。

但是,如果您要进行来回转换,我强烈建议您命名变量以清楚地表明它们的类型,例如:

int iValue, iResult;
unsigned int uValue, uResult;

如果变量没有提示,很容易被更重要的问题分散注意力,忘记哪个变量是什么类型。你不想将其转换为无符号数,然后将其用作数组索引。


-20

糟糕的答案层出不穷

Ozgur Ozcitak

当你从有符号转换为无符号(反之亦然)时,数字的内部表示不会改变。改变的是编译器如何解释符号位。

这完全是错误的。

Mats Fredriksson

当一个无符号变量和一个有符号变量相加(或任何二进制操作),两者都会隐式转换为无符号数,这在这种情况下将导致一个巨大的结果。

这也是错误的。如果由于无符号类型中的填充位具有相等的精度,则无符号整数可以提升为整数。

smh

您的加法操作会导致int转换为unsigned int。

错误。也许它会,也许它不会。

从无符号整数到有符号整数的转换取决于实现。(但是这可能在大多数平台上按照您的期望工作。)

错误。如果它导致溢出,则其行为未定义,否则该值将被保留。

匿名

i的值被转换为无符号整数...

错误。这取决于int相对于unsigned int的精度。

Taylor Price

正如之前回答的那样,你可以在有符号和无符号之间自由地进行转换,没有问题。

错误。尝试存储超出有符号整数范围的值会导致未定义的行为。

现在我终于可以回答这个问题了。

如果int的精度等于unsigned int,则u将被提升为有符号int,并且您将从表达式(u + i)中获得值-4444。现在,如果u和i具有其他值,则可能会发生溢出和未定义的行为,但使用这些确切数字,您将获得-4444 [1] 。此值将具有int类型。但是,您正在尝试将该值存储到无符号整数中,因此将被强制转换为无符号整数,结果将具有(UINT_MAX + 1)- 4444的值。

如果无符号整数的精度大于有符号整数,则有符号整数将被提升为无符号整数,产生值(UINT_MAX+1) - 5678,该值将添加到另一个无符号整数1234中。如果u和i具有使表达式超出范围{0..UINT_MAX}的其他值,则将添加或减去值(UINT_MAX+1),直到结果在范围{0..UINT_MAX}内,并且不会发生未定义行为。

什么是精度?

整数具有填充位、符号位和值位。无符号整数显然没有符号位。无符号字符进一步保证没有填充位。整数具有的值位数就是它的精度。

[注意事项]

如果存在填充位,则单独使用sizeof宏无法确定整数的精度。而且,字节的大小不必像C99定义的那样是八位字节。

[1] 溢出可能发生在两个点之一。要么是在加法之前(在提升期间)- 当您有一个无符号整数太大而无法适应int时。即使无符号整数在int范围内,溢出也可能发生在加法之后,因为加法后的结果仍然可能会溢出。


6
“未签名整数可以提升为整数”这句话是不正确的。因为这些类型已经具有大于或等于int的等级,所以不会发生任何整数提升。根据C语言标准6.3.1.1:“任何无符号整数类型的等级应等于相应有符号整数类型的等级(如果有的话)”。并且根据6.3.1.8:“否则,如果具有无符号整数类型的操作数的等级大于或等于另一个操作数类型的等级,则带有有符号整数类型的操作数将转换为带有无符号整数类型的操作数的类型。”这两个规定保证了在通常的算术转换中int会被转换为unsigned int - CB Bailey
1
6.3.1.1 整数提升用于将一些整数类型(不是 intunsigned int)转换为其中一个类型,其中期望使用 unsigned intint 类型的值。 "或等于" 在 TC2 中添加,以允许具有与 intunsigned int 转换等级相等的枚举类型被转换为这些类型之一。 它从未打算使所描述的提升在 unsigned intint 之间进行转换。 unsigned intint 之间的公共类型确定仍由 6.3.1.8 管理,即使在 TC2 之后也是如此。 - CB Bailey
24
在批评他人的错误回答时发布错误的回答,这似乎不是获取工作的好策略...;-) - R.. GitHub STOP HELPING ICE
1
@Charles Bailey:我同意这确实是标准中的意图,但在TC2之后,这不再是明文语言的效果。我建议需要一份缺陷报告,以添加语言到6.3.1.1p2中的第一个要点,指出它不适用于“int”或“unsigned int”。 - caf
9
我不会投票删除,因为这种错误程度加上傲慢态度太有趣了。 - M.M
显示剩余3条评论

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