Чому 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);
        }
    }
}

Коли я запускаю цю програму, все , що я бачу 0на iв моєму виході. Я б очікував, що ми відбулися б у перший раз i = 1 + 1, за ним i = 2 + 2, за ним i = 4 + 4і т.д.

Це пов’язано з тим, що як тільки ми намагаємося повторно оголосити iзліва, його значення стає скинутим 0?

Якщо хтось може вказати мені на точніші деталі цього, це було б чудово.

Змініть intна longі, здається, друкують номери, як очікувалося. Я здивований тим, як швидко він досягає максимального 32-бітного значення!

Відповіді:


168

Проблема через переповнення цілого числа.

У 32-розрядної арифметиці з двома комплементами:

iнасправді починається з значення двох потужностей, але потім поведінка переповнення починається, як тільки ви досягнете 2 30 :

2 30 + 2 30 = -2 31

-2 31 + -2 31 = 0

... в intарифметиці, оскільки це по суті арифметична мода 2 ^ 32.


28
Чи можете ви трохи розширити свою відповідь?
DeaIss

17
@oOTesterOo Починає друкувати 2, 4 і т. д., але дуже швидко досягає максимального значення цілого числа, і він «загортається» до від’ємних чисел, як тільки він досягне нуля, залишається на нулі назавжди
Річард Тінгл

52
Ця відповідь навіть не є повною (вона навіть не згадує, що значення не буде 0на перших кількох ітераціях, але швидкість виведення затінює цей факт з ОП). Чому це прийнято?
Гонки легкості на орбіті

16
Імовірно, це було прийнято, оскільки ОП вважало корисним.
Джо

4
@LightnessRacesinOrbit Хоча це безпосередньо не стосується питань, які ОП ставить у своєму питанні, відповідь дає достатньо інформації про те, що гідний програміст повинен мати можливість зробити висновок про те, що відбувається.
Кевін

334

Вступ

Проблема в цілому переповненні. Якщо він переповнюється, він повертається до мінімального значення і продовжує звідти. Якщо воно затоплюється, воно повертається до максимального значення і продовжується звідти. Зображення нижче має одометр. Я використовую це для пояснення переповнення. Це механічний перелив, але все ж хороший приклад.

В одометрі значення max digit = 9, що виходить за межі максимального значення 9 + 1, яке переносить і дає a 0; Однак немає більшої цифри, яку можна було б змінити на a 1, тому лічильник скидає zero. Ви розумієте - "цілі числа переповнюються" спадають вам зараз на думку.

введіть тут опис зображення введіть тут опис зображення

Найбільший десятковий буквальний тип int - 2147483647 (2 31 -1). Усі десяткові літерали від 0 до 2147483647 можуть з’являтися де завгодно, а інтралітерал може з’являтися, але буквальний 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" для символічних цілей, але вона також є чудовою наочністю того, як можна було б побачити "цілі переливи".
Алі Гаджані

9
Це моя улюблена відповідь на цьому сайті за всі часи.
Лі Білий

2
Ви, здається, пропустили, що це була подвійна послідовність, а не додавання.
Paŭlo Ebermann

2
Я думаю, що анімація Pacman отримала цю відповідь більше, ніж прийнята відповідь. Попросіть ще один відгук на мене - це одна з моїх улюблених ігор!
Гусман

3
Для всіх, хто не отримав символіку: en.wikipedia.org/wiki/Kill_screen#Pac-Man
wei2912

46

Ні, він не друкує лише нулі.

Змініть це на це, і ви побачите, що станеться.

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

Те, що відбувається, називається переповненням.


61
Цікавий спосіб написати цикл for :)
Бернхард

17
@Bernhard Це, мабуть, зберегти структуру програми ОП.
Taemyr

4
@Taemyr Можливо, але тоді він міг би замінити trueз i<10000:)
Bernhard

7
Я просто хотів додати кілька тверджень; без видалення / зміни будь-яких тверджень. Я здивований, що вона привернула таку широку увагу.
peter.petrov

18
Ви могли б використати прихований оператор у while(k --> 0)розмовному k0
назві

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 та звертається до 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). Переповнення цілочисленних знаків - це невизначена поведінка в C / C ++, на відміну від Java. Будьте дуже обережні, використовуючи подібну конструкцію.
filcab

@filcab: "заміни всю програму чим-небудь", про що ти говориш. Я запустив цю програму на Visual studio 2012, і вона працює ідеально для обох signed and unsignedцілих чисел без жодної невизначеної поведінки
Kaify

3
@Kaify: Працювати добре - це абсолютно справедлива невизначена поведінка. Уявіть, однак, що код робив i += iдля 32+ ітерацій, тоді був if (i > 0). Компілятор може оптимізувати це, if(true)оскільки, якщо ми завжди додаємо додатні числа, iце завжди буде більше 0. Він також може залишити умову в тому, де воно не буде виконуватися, через представлене тут переповнення. Оскільки компілятор може створити дві однаково допустимі програми з цього коду, це не визначене поведінка.
3Doubloons

1
@Kaify: це не лексичний аналіз, це компілятор, який збирає ваш код і, дотримуючись стандарту, може робити "дивні" оптимізації. Як і цикл, про який говорили 3Doubloons. Тільки тому, що компілятори, які ви намагаєтеся завжди щось робити, це не означає, що стандартні гарантії, що ваша програма завжди працюватиме однаково. У вас була не визначена поведінка, якийсь код може бути усунений, оскільки немає способу потрапити туди (UB гарантує це). Ці публікації з блогу llvm (та посилання на них) мають більше інформації: blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
filcab

2
@Kaify: Вибачте за те, що не виклав це, але абсолютно неправильно сказати, що "зберігав це в таємниці", особливо коли це другий результат, в Google, за "невизначене поведінку", який був конкретним терміном, який я використовував для того, що спрацьовувало. .
filcab

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 ітерацій?
Глухий

1
"Не обчислює правильно" - це неправильна характеристика. Розрахунок правильний відповідно до того, що говорить специфікація Java. Справжня проблема полягає в тому, що результат (ідеального) обчислення не може бути представлений як int.
Стівен С

@oOTesterOo - тому що longможе представляти більшу кількість, ніж intможе.
Стівен C

Long має більший асортимент. Тип BigInteger приймає будь-яке значення / довжину, які може виділити ваша JVM.
Бруно Волпато

Я припустив, що int переповнює після 31 ітерації, оскільки його 32-бітове максимальне число, і настільки довге, яке 64-бітне, би досягло свого максимуму після 63? Чому це не так?
Глухий

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

Найбільший десятковий буквальний тип int- 2147483648 (= 2 31 ). Всі десяткові літерали від 0 до 2147483647 можуть з'являтись де завгодно може з'являтись літерал int, але літерал 2147483648 може відображатися лише як операнд унарного оператора заперечення -.

Якщо додавання цілого числа переповнюється, то результатом є біти низького порядку математичної суми, представлені у деяких досить великих форматах двох доповнення. Якщо відбувається переповнення, знаком результату є не те саме, що знаком математичної суми двох значень операнда.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.