是无符号整数减法定义的行为吗?


100

我遇到了某人的代码,该人似乎认为当结果为负数时,从另一个相同类型的整数中减去无符号整数是有问题的。这样的代码即使在大多数架构上都能正常工作,也是不正确的。

unsigned int To, Tf;

To = getcounter();
while (1) {
    Tf = getcounter();
    if ((Tf-To) >= TIME_LIMIT) {
        break;
    } 
}

这是我能从C标准中找到的唯一含糊的相关报价。

涉及无符号操作数的计算永远不能溢出,因为无法用所得的无符号整数类型表示的结果的模数要比该所得的类型可以表示的最大值大一模。

我想可以用这个引号来表示,当右操作数较大时,该操作将被调整为在取模截数的上下文中有意义。

为0x0000 - 0x0001的== 0X 1 0000 - 0x0001的== 0xFFFF的

与使用依赖于实现的签名语义相反:

0x0000-0x0001 ==(无符号)(0 + -1)==(0xFFFF也是0xFFFE或0x8001)

哪种解释正确?完全定义了吗?


3
在标准中选择单词是不幸的。它“永远不会溢出”意味着这不是错误情况。使用标准中的术语,而不是溢出值“ wraps”。
danorton 2011年

Answers:


107

减法生成无符号类型的负数的结果是明确定义的:

  1. 涉及无符号操作数的计算永远不会溢出,因为不能用所得的无符号整数类型表示的结果的模数要比该所得类型可以表示的最大值大一模。(ISO / IEC 9899:1999(E)§6.2.5/ 9)

如您所见,(unsigned)0 - (unsigned)1等于-1模UINT_MAX + 1,换句话说就是UINT_MAX。

请注意,尽管它确实说“涉及无符号操作数的计算永远不会溢出”,这可能使您相信它仅适用于超过上限,但这是对句子的实际绑定部分的一种动机:不能用结果无符号整数类型表示的结果的模数要比可以用结果类型表示的最大值大一的模数减少。” 该短语不仅限于类型上限的溢出,并且同样适用于太低而无法表示的值。


2
谢谢!我现在看到我所缺少的解释。我认为他们可以选择更清晰的措词。

4
我现在感觉好多了,因为他们知道,如果周围的任何未签名除了滚动到零,导致混乱,那将是因为uint总是用来表示数学整数的0通过UINT_MAX,有加法和乘法模的操作UINT_MAX+1,并没有因为溢出。但是,它确实引起了一个问题,即如果环是这样的基本数据类型,为什么该语言没有为其他尺寸的环提供更一般的支持。
西奥多·默多克

2
@TheodoreMurdock我认为该问题的答案很简单。据我所知,这是环的事实是结果,而不是原因。真正的要求是,无符号类型必须让其所有位都参与值表示。环状行为自然而然地产生了。如果您希望其他类型的行为如此,那么请进行算术运算,然后应用所需的模数;使用基本运算符。
underscore_d

@underscore_d当然……很明显,他们为什么要做出设计决定。他们将规范粗略地写为“没有算术上溢/下溢,因为数据类型被指定为环形”,这很可笑,好像这种设计选择意味着程序员不必小心避免上溢和下溢-flow或程序失败。
西奥多·默多克

120

当您使用无符号类型时,会发生模块化算术(也称为“环绕”行为)。要了解这种模块化算法,只需看一下这些时钟:

在此处输入图片说明

9 + 4 = 113 mod 12),所以在另一个方向上是:1-4 = 9-3 mod 12)。使用无符号类型时,将应用相同的原理。如果结果类型unsigned,则进行模块化算术运算。


现在查看将结果存储为的以下操作unsigned int

unsigned int five = 5, seven = 7;
unsigned int a = five - seven;      // a = (-2 % 2^32) = 4294967294 

int one = 1, six = 6;
unsigned int b = one - six;         // b = (-5 % 2^32) = 4294967291

如果要确保结果为signed,则将其存储到signed变量中或将其强制转换为signed。当您想获取数字之间的差异并确保不应用模块化算术时,则应考虑使用abs()在中定义的函数stdlib.h

int c = five - seven;       // c = -2
int d = abs(five - seven);  // d =  2

要特别小心,尤其是在编写条件时,因为:

if (abs(five - seven) < seven)  // = if (2 < 7)
    // ...

if (five - seven < -1)          // = if (-2 < -1)
    // ...

if (one - six < 1)              // = if (-5 < 1)
    // ...

if ((int)(five - seven) < 1)    // = if (-2 < 1)
    // ...

if (five - seven < 1)   // = if ((unsigned int)-2 < 1) = if (4294967294 < 1)
    // ...

if (one - six < five)   // = if ((unsigned int)-5 < 5) = if (4294967291 < 5)
    // ...

4
不错的选择,尽管有证据证明这是正确的答案。问题的前提已经包括所有这些可能都是正确的断言。
Lightness Races in Orbit

5
@LightnessRacesinOrbit:谢谢。我写它是因为我认为有人可能会觉得很有帮助。我同意,这不是一个完整的答案。
LihO 2013年

4
这条线int d = abs(five - seven);不好。首先five - seven进行计算:提升将操作数类型保留为unsigned int,对结果进行取模运算(UINT_MAX+1)并求和为UINT_MAX-1。然后,此值是的实际参数abs,这是个坏消息。 abs(int)导致传递参数的未定义行为,因为它不在范围内,并且abs(long long)可能保存该值,但是当将返回值强制int初始化为时,会发生未定义行为d
Ben Voigt

1
@LihO:C ++中唯一与上下文相关并且根据其结果使用方式不同而有所不同的运算符是自定义转换运算符operator T()。我们正在讨论的两个表达式中的加法是unsigned int根据操作数类型在type中执行的。相加的结果是unsigned int。然后,将结果隐式转换为上下文所需的类型,转换失败,因为该值无法在新类型中表示。
Ben Voigt 2015年

1
@LihO:想起double x = 2/3;vsdouble y = 2.0/3;
Ben Voigt

5

好吧,第一种解释是正确的。但是,您在这种情况下对“签名语义”的推理是错误的。

同样,您的第一个解释是正确的。无符号运算后续模运算规则,这意味着0x0000 - 0x0001计算结果为0xFFFF32位无符号类型。

但是,还需要第二种解释(一种基于“签名语义”的解释)才能产生相同的结果。即,即使您0 - 1在带符号类型的域中求值并获得-1作为中间结果,-1也需要0xFFFF在稍后将其转换为无符号类型时产生此结果。即使某些平台使用带符号的整数表示带符号的整数(1的补码,带符号的幅度),当将带符号的整数值转换为无符号的整数值时,仍要求该平台应用模算术规则。

例如,此评估

signed int a = 0, b = 1;
unsigned int c = a - b;

仍保证生产UINT_MAXc,即使该平台采用异国情调的代表符号整数。


4
我认为您的意思是16位无符号类型,而不是32位。
xioxox

4

对于无符号类型unsigned int或更大的数字,在没有类型转换的情况下,a-b将其定义为产生无符号数字b,将其添加到后将产生yield a。负数到无符号的转换被定义为产生的数字,当将其添加到带符号反转的原始数字时,将产生零(因此将-5转换为无符号将产生一个值,当加到5时将产生零) 。

请注意,小于的无符号数unsigned int可能会int在减法运算前被提升为类型,的行为a-b将取决于的大小int

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.