【问题标题】:Why does i = i + i give me 0?为什么 i = i + i 给我 0?
【发布时间】:2023-03-17 01:51:02
【问题描述】:

我有一个简单的程序:

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

当我运行这个程序时,我在输出中看到的只是0 for i。我预计第一轮我们会有i = 1 + 1,然后是i = 2 + 2,然后是i = 4 + 4等等。

这是因为一旦我们尝试在左侧重新声明i,它的值就会重置为0

如果有人能告诉我更详细的信息,那就太好了。

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

【问题讨论】:

    标签: java math variable-assignment


    【解决方案1】:

    这是正确的,但经过 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次迭代后没有遇到这个问题?
    • “计算不正确”是不正确的表征。根据 Java 规范所说的应该发生的计算是正确的。真正的问题是(理想)计算的结果不能表示为int
    • @oOTesterOo - 因为long 可以表示比int 更大的数字。
    • Long 的范围更大。 BigInteger 类型接受 JVM 可以分配的任何值/长度。
    • 我假设 int 在 31 次迭代后溢出,因为它是一个 32 位的最大大小的数字,那么 64 位的长度会在 63 之后达到其最大值吗?为什么不是这样?
    【解决方案2】:

    问题是由于整数溢出。

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

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

    230 + 230 = -231

    -231 + -231 = 0

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

    【讨论】:

    • 你能扩展一下你的答案吗?
    • @oOTesterOo 它开始打印 2、4 等,但它很快达到整数的最大值并“环绕”为负数,一旦达到零,它就永远保持为零跨度>
    • 这个答案甚至不完整(它甚至没有提到在前几次迭代中该值将0 ,但输出速度从 ​​OP 中掩盖了这一事实)。为什么会被接受?
    • 可能它被接受了,因为它被 OP 认为有帮助。
    • @LightnessRacesinOrbit 虽然它没有直接解决 OP 在他们的问题中提出的问题,但答案提供了足够的信息,一个体面的程序员应该能够推断出发生了什么。
    【解决方案3】:

    简介

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

    在里程表中,max digit = 9,因此超出最大值意味着9 + 1,它会继续并给出0;但是没有更高的数字可以更改为1,因此计数器重置为zero。你明白了 - 现在想到“整数溢出”。

    int 类型的最大十进制文字是 2147483647 (231-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
    

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

    Original PDF

    【讨论】:

    • 我已经为“吃豆人”添加了动画以用于象征性目的,但它也可以很好地展示人们如何看待“整数溢出”。
    • 这是我在这个网站上最喜欢的答案。
    • 您似乎忽略了这是一个加倍序列,而不是添加一个。
    • 我认为吃豆人动画得到的这个答案比接受的答案更多。再给我点个赞吧——这是我最喜欢的游戏之一!
    • 对于没有得到象征意义的任何人:en.wikipedia.org/wiki/Kill_screen#Pac-Man
    【解决方案4】:

    我将使用一个 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
    ...
    

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

        【解决方案6】:

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

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

        这里的起始值为1,所以如果我们用8位数字来存储i(例如),

        • 0次迭代后,值为00000001
        • 1次迭代后,值为00000010
        • 经过2次迭代,值为00000100

        以此类推,直到最后的非零步骤

        • 7次迭代后,值为10000000
        • 8次迭代后,值为00000000

        无论分配多少二进制数字来存储数字,无论起始值是什么,最终所有数字都会被推到左边而丢失。在那之后,继续将数字加倍不会改变数字 - 它仍将由全零表示。

        【讨论】:

          【解决方案7】:

          由于我没有足够的声誉,我无法在 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;
          }
          

          【讨论】:

          • 这个程序,在 C 中,实际上在每次执行时都会触发未定义的行为,这允许编译器用任何东西替换整个程序(即使是 system("deltree C:"),因为你在 DOS/Windows 中)。与 Java 不同,有符号整数溢出是 C/C++ 中未定义的行为。使用这种结构时要非常小心。
          • @filcab : “用任何东西替换整个程序”你在说什么。我已经在 Visual Studio 2012 上运行了这个程序,它对于两个 signed and unsigned 整数都运行得非常好,没有任何 未定义的行为
          • @Kaify:正常工作是一种完全有效的未定义行为。然而想象一下,代码执行了i += i 32+ 次迭代,然后有if (i &gt; 0)。编译器可以将其优化为if(true),因为如果我们总是添加正数,i 将始终大于 0。它还可以保留条件,因为表示溢出,它不会被执行这里。因为编译器可以从该代码生成两个同样有效的程序,所以这是未定义的行为。
          • @Kaify:这不是词法分析,而是编译器编译您的代码,并且按照标准,能够进行“奇怪”的优化。就像 3Doubloons 所说的循环一样。仅仅因为您尝试的编译器似乎总是在做某事,这并不意味着标准保证您的程序将始终以相同的方式运行。你有未定义的行为,一些代码可能已经被删除,因为没有办法到达那里(UB保证)。这些来自 llvm 博客的帖子(以及其中的链接)包含更多信息:blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
          • @Kaify:很抱歉没有把它说出来,但是说“保密”是完全错误的,尤其是当它是谷歌的第二个结果时,“未定义的行为”,这是具体的我用于触发事件的术语。
          【解决方案8】:
          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;
          

          【讨论】:

          • 嗯,不,它只适用于这个特定的序列达到零。尝试从 3 开始。
          【解决方案9】:

          对于调试此类情况,最好减少循环中的迭代次数。用这个代替你的while(true)

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

          然后您可以看到它以 2 开头,并且将值加倍直到导致溢出。

          【讨论】:

            【解决方案10】:

            不,它不只打印零。

            把它改成这个,你会看到会发生什么。

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

            发生的事情称为溢出。

            【讨论】:

            • 写 for 循环的有趣方式 :)
            • @Bernhard 可能是为了保留OP程序的结构。
            • @Taemyr 可能,但他可以用true 替换i&lt;10000 :)
            • 我只是想补充几句;不删除/更改任何语句。我很惊讶它会引起如此广泛的关注。
            • 您可以使用隐藏运算符while(k --&gt; 0),俗称“而k 转到0”;)
            猜你喜欢
            • 2012-11-11
            • 1970-01-01
            • 1970-01-01
            • 2011-06-14
            • 2017-06-19
            • 1970-01-01
            • 2012-08-25
            • 2019-03-11
            • 2015-12-21
            相关资源
            最近更新 更多