为什么我=我+我给我0?


96

我有一个简单的程序:

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

当我运行该程序时,我看到的只是0用于i输出。我原本希望我们会遇到第一轮i = 1 + 1,然后是i = 2 + 2,然后是i = 4 + 4etc。

这是由于这样的事实,我们尝试i在左侧重新声明时,其值会重置为0

如果有人可以指出我的详细情况,那将很棒。

更改intlong,似乎正在按预期方式打印数字。我对它达到最大32位值的速度感到惊讶!

Answers:


168

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

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

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

2 30 + 2 30 = -2 31

-2 31 + -2 31 = 0

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


28
您能否扩大答案范围?
DeaIss 2014年

17
@oOTesterOo它开始打印2、4等,但很快就达到整数的最大值,并且“回绕”为负数,一旦达到零,它将永远保持零
Richard Tingle

52
这个答案甚至还不完整(它甚至没有提到该值不会出现0在前几次迭代中,但是输出速度掩盖了OP中的事实)。为什么接受?
Lightness Races in Orbit 2014年

16
大概因为它被OP认为有用而被接受。
2014年

4
@LightnessRacesinOrbit尽管它不能直接解决OP在他们的问题中提出的问题,但答案给出了足够的信息,一个体面的程序员应该能够推断出正在发生的事情。
凯文

334

介绍

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

在里程表中,max digit = 9超出最大均值9 + 1,该值会延续并给出0;但是,没有更高的数字可以更改为a 1,因此计数器重置为zero。您知道了-现在想到“整数溢出”。

在此处输入图片说明 在此处输入图片说明

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
我已经添加了用于“吃豆子”的动画,以用于象征性目的,但它也很好地展示了人们如何看待“整数溢出”。
Ali Gajani 2014年

9
这是我一直以来最喜欢这个网站的答案。
李·怀特

2
您似乎已经错过了这是一个加倍序列,而不是加一个序列。
圣保罗Ebermann

2
我认为吃豆子动画比接受的答案更能回答这个问题。对我有另一个赞誉-这是我最喜欢的游戏之一!
Husman 2014年

3
对于任何没有获得象征意义的人:en.wikipedia.org/wiki/Kill_screen#
吃豆人

46

不,它不会只输出零。

将其更改为此,您将看到会发生什么。

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

发生的事情称为溢出。


61
编写for循环的有趣方式:)
Bernhard 2014年

17
@Bernhard可能要保留OP程序的结构。
Taemyr 2014年

4
@Taemyr可能,但随后他可以代替truei<10000:)
伯恩哈德

7
我只想添加一些声明;而不删除/更改任何声明。我很惊讶它吸引了如此广泛的关注。
peter.petrov 2014年

18
您本可以使用while(k --> 0)通俗地称为“ while kgo to 0” 的隐藏运算符;)
Laurent LA RIZZA 2014年

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ŭloEbermann 2014年

4

由于我没有足够的声誉,所以我无法在C语言中以受控输出发布同一程序的输出图片,因此您可以尝试一下,看看它实际打印了32次,然后由于溢出i = 1073741824 + 1073741824进行了解释更改为 -2147483648,又有一个加法超出int的范围,并变为Zero

#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中也是如此)。与Java不同,带符号整数溢出是C / C ++中未定义的行为。使用这种构造时要非常小心。
filcab 2014年

@filcab:“用任何东西替换整个程序”您在说什么。我已经在Visual Studio 2012上运行了该程序,并且对于两个signed and unsigned整数都运行得很好,没有任何未定义的行为
Kaify 2014年

3
@Kaify:工作正常是一种完全有效的不确定行为。但是,想象一下,该代码进行i += i了32次以上的迭代,然后有了if (i > 0)。编译器可以对此进行优化,if(true)因为如果我们一直加正数,i则它总是大于0。由于这里表示的溢出,它也可能使条件不执行,而不会执行。因为编译器可以从该代码生成两个同等有效的程序,所以它的行为不确定。
3Doubloons 2014年

1
@Kaify:这不是词法分析,而是编译器编译您的代码,并且遵循标准,能够执行“怪异”的优化。就像3Doubloons谈到的循环一样。仅仅因为您尝试的编译器似乎总是在做某事,并不意味着该标准保证您的程序将始终以相同的方式运行。您有未定义的行为,由于无法到达那里,某些代码可能已被消除(UB保证了这一点)。这些来自llvm博客的帖子(及其中的链接)具有更多信息:blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
filcab 2014年

2
@Kaify:不好意思,但是说“保守秘密”是完全错误的,特别是当它在Google上针对“未定义的行为”的第二个结果时,这是我所触发的特定术语。
filcab 2014年

4

的值i使用固定数量的二进制数字存储在内存中。当一个数字需要的数字多于可用数字时,仅存储最低的数字(丢失最高的数字)。

添加i到本身是一样的乘i用两个。就像将十进制数乘以十可以通过将每个数字向左滑动并在右边置零来执行一样,将二进制数乘以二可以用相同的方式执行。这会在右侧添加一个数字,因此左侧会丢失一个数字。

此处的起始值为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);
        }
    }
}

如果我使用long而不是int,那么很长时间以来,它似乎一直打印> 0数字。为什么经过63次迭代后仍然没有遇到这个问题?
DeaIss 2014年

1
“计算不正确”是不正确的表征。根据Java规范的说法,该计算是正确的。真正的问题是(理想)计算的结果不能表示为int
Stephen C

@oOTesterOo-因为long可以代表更大的数字int
Stephen C

长范围更大。BigInteger类型接受JVM可以分配的任何值/长度。
Bruno Volpato 2014年

我认为int在31次迭代后会溢出,因为它的最大大小为32位,而这么长的64位会在63次后达到其最大值?为什么不是这样?
DeaIss 2014年

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

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

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

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.