为什么 i = i + i 会得到 0?

96

我有一个简单的程序:

public class Mathz {
    static int i = 1;
    public static void main(String[] args) {    
        while (true){
            i = i + i;
            System.out.println(i);
        }
    }
}
当我运行这个程序时,输出中对于i的值始终为0。我本以为第一次循环后i = 1 + 1,然后是i = 2 + 2,接着是i = 4 + 4等等。
这是因为只要我们尝试在左侧重新声明i,它的值就会被重置为0
如果有人能为我指出更细节的问题,那就太好了。
int更改为long,似乎可以按预期打印数字。我惊讶于它如此快地达到了最大32位值!
10个回答

334

介绍

问题是整数溢出。如果发生溢出,则会回到最小值并从那里继续。如果发生下溢,则会回到最大值并从那里继续。下面的图像是一个里程表。我用它来解释溢出。虽然这是一个机械性溢出,但仍然是一个很好的例子。

在里程表中,最大数字=9,因此超过最大值意味着9 + 1,这样就会进位并给出0 。然而,没有更高的数字可以改变为1,因此计数器重置为。您明白了——现在想到了“整数溢出”。

enter image description here enter image description here

类型为int的最大十进制字面量为2147483647(2 31-1)。所有0到2147483647的十进制字面量都可以出现在int字面量可能出现的任何位置,但字面量2147483648只能作为一元否定操作符-的操作数出现。

如果整数加法溢出,则结果是在某些足够大的二进制补码格式中表示的数学和的低阶位数。如果发生溢出,则结果的符号与两个操作数值的数学总和的符号不同。

因此,2147483647 + 1会溢出并绕回-2147483648。因此int i=2147483647 + 1会溢出,而不等于2147483648。此外,您说“它总是打印0”。实际上并非如此,因为http://ideone.com/WHrQIW。下面这8个数字显示了它开始旋转并溢出的点。然后它开始打印0。另外,不要对今天的计算机速度感到惊讶,它们非常快。

268435456
536870912
1073741824
-2147483648
0
0
0
0

整数溢出为什么会“环绕”?

原始PDF


17
我已添加"Pacman"动画以符号化表达,但它也是一个展示"整数溢出"的很好的视觉效果。 - Ali Gajani
9
这是我在这个网站上有史以来最喜欢的答案。 - Lee White
2
你似乎忽略了这是一个翻倍序列,而不是加一。 - Paŭlo Ebermann
2
我认为Pacman动画比被接受的答案获得了更多的赞同。再给你一次赞 - 这是我最喜欢的游戏之一! - Husman
3
如果有人不懂象征意义,请参考:https://en.wikipedia.org/wiki/Kill_screen#Pac-Man - wei2912
显示剩余7条评论

168

问题是由于整数溢出引起的。

在32位二进制补码算术中:

i 的值确实开始具有2的幂次方,但一旦达到2的30次方,就会出现溢出行为:

230 + 230 = -231

-231 + -231 = 0

...在int算术中,因为它本质上是模2^32算术。


28
你能否稍微详细地阐述一下你的回答? - DeaIss
17
它开始打印2、4等数字,但很快就达到了整数的最大值,并"回绕"成负数,一旦它达到零,就会永远停留在零。 - Richard Tingle
52
这个答案甚至不完整(它甚至没有提到在前几次迭代中该值将不会是“0”,但输出速度掩盖了这一事实,让问题提出者感到困惑)。为什么它被接受了? - Lightness Races in Orbit
16
可能之所以被接受,是因为问问题的人认为它有帮助。 - Joe
4
尽管它没有直接回答提问者在问题中提出的问题,但这个答案提供了足够的信息,使一个合格的程序员能够推断出正在发生什么。 - Kevin
显示剩余5条评论

46

不,它不会只打印零。

将其更改为以下内容,您就会看到发生了什么。

    int k = 50;
    while (true){
        i = i + i;
        System.out.println(i);
        k--;
        if (k<0) break;
    }

发生的情况被称为溢出。


61
有趣的写 for 循环的方式 :) - Bernhard
17
很可能是为了保留原帖作者程序的结构。 - Taemyr
4
也许是这样,但他可以将“true”替换为“i<10000” :) - Bernhard
7
我只想添加几个声明,不删除/更改任何声明。我很惊讶它引起了如此广泛的关注。 - peter.petrov
18
你本可以使用隐藏运算符 while(k --> 0),俗称为 "当 k 递减至 0 时" ;) - Laurent LA RIZZA

15
static int i = 1;
    public static void main(String[] args) throws InterruptedException {
        while (true){
            i = i + i;
            System.out.println(i);
            Thread.sleep(100);
        }
    }

输出:

2
4
8
16
32
64
...
1073741824
-2147483648
0
0

when sum > Integer.MAX_INT then assign i = 0;

4
不,这只适用于达到零的特定序列。请尝试从3开始。 - Paŭlo Ebermann

4

由于我的声望不够,我无法发布C语言程序的输出图片,您可以自己尝试并查看它实际上打印了32次,然后由于溢出而导致 i = 1073741824 + 1073741824 变成了 -2147483648,再加一次就超出了int范围并变成了

#include<stdio.h>
#include<conio.h>

int main()
{
static int i = 1;

    while (true){
        i = i + i;
      printf("\n%d",i);
      _getch();
    }
      return 0;
}

3
这个用C语言编写的程序,在每次运行时实际上都会触发未定义行为,这使得编译器可以将整个程序替换为任何东西(甚至是system("deltree C:"),因为你在DOS / Windows中)。 在C / C ++中,有符号整数溢出属于未定义行为,而不像Java。 当使用这种结构时要非常小心。 - filcab
@filcab: 你在说什么“用任何东西替换整个程序”?我已经在Visual Studio 2012上运行了这个程序,对于有符号和无符号的整数都可以完美地运行,没有任何未定义的行为。 - Kaify
3
“工作正常”是一种完全合法但未定义的行为。但是,如果代码在32次以上的迭代中执行了i += i,然后有一个if (i > 0)语句,编译器可能会将其优化为if(true),因为如果我们始终添加正数,i将始终大于0。它也可以将条件保留下来,但不会执行,因为这里表示的溢出。因为编译器可以从该代码生成两个同样有效的程序,所以它是未定义的行为。 - 3Doubloons
1
@Kaify:这不是词法分析,而是编译器编译您的代码,并且遵循标准,能够进行“奇怪”的优化。就像3Doubloons所说的那个循环一样。仅仅因为您尝试的编译器似乎总是做某些事情,并不意味着标准保证您的程序始终以相同的方式运行。您的代码存在未定义行为,某些代码可能已被消除,因为无法到达(UB保证了这一点)。这些来自llvm博客的帖子(以及其中的链接)提供了更多信息:http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html - filcab
2
@Kaify:很抱歉没有说明,但是说“保守秘密”完全是错误的,特别是当它是谷歌上“未定义行为”的第二个结果时,而这正是我用来描述所触发的问题的具体术语。 - filcab
显示剩余4条评论

4
i的值使用固定数量的二进制位存储在内存中。当一个数字需要比可用的位数更多的位时,只有最低位被存储(高位将丢失)。
i加倍相当于将i乘以2。就像在十进制符号下将数字乘以十可以通过将每个数字向左滑动并在右侧放置零来执行一样,在二进制符号下将数字乘以2也可以执行同样的操作。这增加了一个位于右侧的数字,因此左边会丢失一个数字。
这里的起始值为1,因此如果我们使用8位存储i(例如),
  • 0次迭代后,值为00000001
  • 1次迭代后,值为00000010
  • 2次迭代后,值为00000100
等等,直到最终的非零步骤,
  • 7次迭代后,值为10000000
  • 8次迭代后,值为00000000
无论分配多少二进制位来存储数字,无论起始值是什么,最终所有位都将因向左推移而丢失。在该点之后,继续将数字加倍将不会改变数字 - 它仍将由所有零表示。

3

这是正确的,但在31次迭代后,1073741824 + 1073741824无法正确计算(溢出),此后只会打印0。

你可以重构代码来使用BigInteger,这样你的无限循环将能够正确工作。

public class Mathz {
    static BigInteger i = new BigInteger("1");
    
    public static void main(String[] args) {    
        
        while (true){
            i = i.add(i);
            System.out.println(i);
        }
    }
}

1
“不正确计算”是一种不正确的描述。根据Java规范,计算是正确的。真正的问题是(理想情况下的)计算结果无法表示为“int”。 - Stephen C
@oOTesterOo - 因为 long 可以表示比 int 更大的数字。 - Stephen C
你是正确的。请查看http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html。 - Bruno Volpato
抱歉,我仍然不明白为什么当我使用 long 时,在 63 次迭代后没有看到“溢出”或“最大”值的出现。当我这样做时,它似乎在 63 次迭代之后仍然继续变大! - DeaIss
整数的最大值为2^31-1,长整型为2^63-1,因此在第63次迭代中会出现问题。使用BigInteger的示例将按预期工作。 - Bruno Volpato
显示剩余3条评论

2

在调试这种情况时,减少循环迭代次数是一个好方法。使用以下代码替换while(true)

for(int r = 0; r<100; r++)

你可以看到它以2开头,并且将该值加倍,直到引起溢出。

2

我将使用一个8位数字进行说明,因为它可以在短时间内完全详细地描述。十六进制数以0x开头,而二进制数以0b开头。

8位无符号整数的最大值为255(0xFF或0b11111111)。 如果加1,则通常会期望得到:256(0x100或0b100000000)。 但由于这太多位了(9位),超过了最大值,所以第一部分就被丢弃了,留下了有效的0(0x(1)00或0b(1)00000000,但1被丢掉了)。

因此,当您的程序运行时,您会得到:

1 = 0x01 = 0b1
2 = 0x02 = 0b10
4 = 0x04 = 0b100
8 = 0x08 = 0b1000
16 = 0x10 = 0b10000
32 = 0x20 = 0b100000
64 = 0x40 = 0b1000000
128 = 0x80 = 0b10000000
256 = 0x00 = 0b00000000 (wraps to 0)
0 + 0 = 0 = 0x00 = 0b00000000
0 + 0 = 0 = 0x00 = 0b00000000
0 + 0 = 0 = 0x00 = 0b00000000
...

1
< p>类型为int的最大十进制字面量是2147483648(= 2 31)。所有从0到2147483647的十进制字面量都可以出现在任何int字面量可能出现的地方,但字面量2147483648只能出现作为一元否定运算符-的操作数。

如果整数加法溢出,则结果是以某些足够大的二补格式表示的数学和的低位比特。如果发生溢出,则结果的符号与两个操作数值的数学和的符号不同。


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