Dlaczego i = i + daje mi 0?


96

Mam prosty program:

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

Kiedy uruchomić ten program, wszystko co widzę to 0na imoim wyjściu. Spodziewałbym się, że za pierwszym razem będziemy mieć i = 1 + 1, a i = 2 + 2następnie i = 4 + 4itd.

Czy wynika to z faktu, że gdy tylko spróbujemy ponownie zadeklarować ipo lewej stronie, jej wartość zostanie zresetowana do 0?

Byłoby wspaniale, gdyby ktokolwiek mógł wskazać mi szczegóły tej sprawy.

Zmienić intsię longi wydaje się być drukowanie numerów zgodnie z oczekiwaniami. Jestem zaskoczony, jak szybko osiąga maksymalną wartość 32-bitową!

Odpowiedzi:


168

Problem wynika z przepełnienia liczb całkowitych.

W 32-bitowej arytmetyce z uzupełnieniem do dwóch:

ifaktycznie zaczyna się od wartości potęgi dwóch, ale potem zachowania związane z przepełnieniem zaczynają się, gdy osiągniesz 2 30 :

2 30 + 2 30 = -2 31

-2 31 + -2 31 = 0

... w intarytmetyce, ponieważ jest to zasadniczo arytmetyczny mod 2 ^ 32.


28
Czy mógłbyś trochę rozwinąć swoją odpowiedź?
DeaIss

17
@oOTesterOo Zaczyna wypisywać 2, 4 itd., ale bardzo szybko osiąga maksymalną wartość liczby całkowitej i „zawija się” do liczb ujemnych, po osiągnięciu zera pozostaje na zawsze na poziomie zero
Richard Tingle

52
Ta odpowiedź nie jest nawet kompletna (nawet nie wspomina, że wartość nie będzie 0w pierwszych kilku iteracjach, ale szybkość wyjścia przesłania ten fakt z PO). Dlaczego jest to akceptowane?
Wyścigi lekkości na orbicie,

16
Przypuszczalnie został przyjęty, ponieważ PO uznał go za pomocny.
Joe

4
@LightnessRacesinOrbit Chociaż nie odnosi się bezpośrednio do problemów postawionych przez OP, odpowiedź dostarcza wystarczających informacji, aby przyzwoity programista był w stanie wywnioskować, co się dzieje.
Kevin

334

Wprowadzenie

Problem polega na przepełnieniu liczb całkowitych. Jeśli się przepełni, wraca do wartości minimalnej i kontynuuje od tego momentu. Jeśli spada, wraca do wartości maksymalnej i kontynuuje od tego momentu. Poniższy obraz przedstawia licznik kilometrów. Używam tego, aby wyjaśnić przepełnienia. To mechaniczny przelew, ale wciąż dobry przykład.

W liczniku kilometrów, max digit = 9więc wykracza poza maksymalne średnie 9 + 1, które przenosi i daje 0; Jednak nie ma wyższej cyfry do zmiany na a 1, więc licznik resetuje się do zero. Masz pomysł - teraz przychodzą mi na myśl „przepełnienia całkowitoliczbowe”.

wprowadź opis obrazu tutaj wprowadź opis obrazu tutaj

Największy dosłownym dziesiętny typu int to 2147483647 (2 31 1). Wszystkie literały dziesiętne od 0 do 2147483647 mogą pojawić się wszędzie tam, gdzie może pojawić się literał int, ale literał 2147483648 może pojawić się tylko jako operand jednoargumentowego operatora negacji -.

Jeśli dodawanie liczb całkowitych przepełnia, wynikiem są najmniejsze bity sumy matematycznej, reprezentowane w jakimś wystarczająco dużym formacie uzupełnienia do dwóch. Jeśli wystąpi przepełnienie, znak wyniku nie jest taki sam, jak znak sumy matematycznej dwóch wartości operandów.

W ten sposób 2147483647 + 1przepełnia się i zawija do -2147483648. Stąd int i=2147483647 + 1byłby przepełniony, co nie jest równe 2147483648. Mówisz też „zawsze wypisuje 0”. Nie, ponieważ http://ideone.com/WHrQIW . Poniżej te 8 liczb pokazuje punkt, w którym obraca się i przepełnia. Następnie zaczyna drukować zera. Nie zdziw się też, jak szybko to oblicza, dzisiejsze maszyny są szybkie.

268435456
536870912
1073741824
-2147483648
0
0
0
0

Dlaczego przepełnienie liczb całkowitych „zawija się”

Oryginalny plik PDF


17
Dodałem animację do „Pacmana” ze względów symbolicznych, ale służy ona również jako świetny obraz pokazujący, jak można zobaczyć „przepełnienia całkowitoliczbowe”.
Ali Gajani,

9
To moja ulubiona odpowiedź zawsze na tej stronie.
Lee White,

2
Wygląda na to, że przegapiłeś, że to była sekwencja podwojenia, a nie dodawanie.
Paŭlo Ebermann

2
Myślę, że animacja Pacmana dostała więcej głosów pozytywnych niż zaakceptowana odpowiedź. Zagłosuj na mnie jeszcze raz - to jedna z moich ulubionych gier!
Husman

3
Dla każdego, kto nie
zrozumiał

46

Nie, nie drukuje tylko zer.

Zmień to na to, a zobaczysz, co się stanie.

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

To, co się dzieje, nazywa się przepełnieniem.


61
Ciekawy sposób na napisanie pętli for :)
Bernhard

17
@Bernhard Prawdopodobnie zachowa strukturę programu OP.
Taemyr,

4
@Taemyr Prawdopodobnie, ale wtedy mógłby zastąpić truez i<10000:)
Bernhard

7
Chciałem tylko dodać kilka stwierdzeń; bez usuwania / zmiany jakichkolwiek wypowiedzi. Dziwię się, że przyciągnął tak szeroką uwagę.
peter.petrov

18
Mogłeś użyć ukrytego operatora, while(k --> 0)potocznie nazwanego „while kidzie do 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);
        }
    }

wynik:

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

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

4
Um, nie, to po prostu działa, aby ta konkretna sekwencja osiągnęła zero. Spróbuj zacząć od 3.
Paŭlo Ebermann

4

Ponieważ nie mam wystarczającej reputacji, nie mogę opublikować zdjęcia wyjścia tego samego programu w C z kontrolowanym wyjściem, możesz spróbować sam i zobaczyć, że faktycznie drukuje 32 razy, a następnie jak wyjaśniono z powodu przepełnienia i = 1073741824 + 1073741824 zmienia się na -2147483648 i jeszcze jeden dodatek jest poza zakresem int i zmienia się w 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
Ten program w C faktycznie wyzwala niezdefiniowane zachowanie przy każdym wykonaniu, co pozwala kompilatorowi na zastąpienie całego programu czymkolwiek (nawet system("deltree C:")jeśli jesteś w DOS / Windows). Przepełnienie podpisanych liczb całkowitych jest niezdefiniowanym zachowaniem w C / C ++, w przeciwieństwie do Java. Zachowaj ostrożność podczas korzystania z tego rodzaju konstrukcji.
filcab

@filcab: "zamień cały program na cokolwiek" o czym mówisz. Uruchomiłem ten program na Visual studio 2012 i działa on doskonale dla obu signed and unsignedliczb całkowitych bez żadnego niezdefiniowanego zachowania
Kaify,

3
@Kaify: Praca bez zarzutu to całkowicie prawidłowe niezdefiniowane zachowanie. Wyobraź sobie jednak, że kod i += idziałał przez ponad 32 iteracje, a potem miał if (i > 0). Kompilator może to zoptymalizować, if(true)ponieważ jeśli zawsze dodajemy liczby dodatnie, izawsze będzie większe niż 0. Może również pozostawić warunek w miejscu, w którym nie zostanie wykonany, z powodu przedstawionego tutaj przepełnienia. Ponieważ kompilator mógł stworzyć dwa równie ważne programy z tego kodu, jest to niezdefiniowane zachowanie.
3Doubloons

1
@Kaify: to nie jest analiza leksykalna, to kompilator, który kompiluje Twój kod i, zgodnie ze standardem, jest w stanie wykonać „dziwne” optymalizacje. Podobnie jak pętla, o której mówił 3Doubloons. Tylko dlatego, że kompilatory, których próbujesz, zawsze wydają się coś robić, nie oznacza to, że standard gwarantuje, że Twój program będzie zawsze działał w ten sam sposób. Miałeś niezdefiniowane zachowanie, część kodu mogła zostać wyeliminowana, ponieważ nie ma sposobu, aby się tam dostać (UB gwarantuje to). Te posty z bloga llvm (i tam zawarte linki) zawierają więcej informacji: blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
filcab

2
@Kaify: Przepraszamy, że tego nie powiedziałem, ale mówienie „zachował to w tajemnicy” jest całkowicie błędne, zwłaszcza gdy jest to drugi wynik w Google dotyczący „nieokreślonego zachowania”, który był specyficznym terminem, którego użyłem do określenia tego, co było uruchamiane .
filcab

4

Wartość ijest przechowywana w pamięci przy użyciu ustalonej liczby cyfr binarnych. Gdy numer wymaga więcej cyfr niż jest dostępnych, przechowywane są tylko najniższe cyfry (najwyższe cyfry zostaną utracone).

Dodawanie ido siebie jest tym samym, co mnożenie iprzez dwa. Podobnie jak pomnożenie liczby przez dziesięć w notacji dziesiętnej można wykonać przesuwając każdą cyfrę w lewo i wstawiając zero po prawej stronie, tak samo można wykonać pomnożenie liczby przez dwa w notacji binarnej. Spowoduje to dodanie jednej cyfry po prawej stronie, więc cyfra zostanie utracona po lewej stronie.

Tutaj wartością początkową jest 1, więc jeśli użyjemy 8 cyfr do przechowywania i(na przykład),

  • po 0 iteracjach wartość wynosi 00000001
  • po 1 iteracji wartość wynosi 00000010
  • po 2 iteracjach wartość wynosi 00000100

i tak dalej, aż do ostatniego niezerowego kroku

  • po 7 iteracjach wartość wynosi 10000000
  • po 8 iteracjach wartość wynosi 00000000

Bez względu na to, ile cyfr binarnych jest przydzielonych do przechowywania numeru i bez względu na wartość początkową, ostatecznie wszystkie cyfry zostaną utracone, gdy zostaną przesunięte w lewo. Po tym czasie dalsze podwojenie liczby nie zmieni liczby - nadal będzie reprezentowana przez wszystkie zera.


3

To prawda, ale po 31 iteracjach 1073741824 + 1073741824 nie oblicza poprawnie i po tym drukuje tylko 0.

Możesz refaktoryzować, aby użyć BigInteger, aby Twoja nieskończona pętla działała poprawnie.

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);
        }
    }
}

Jeśli użyję long zamiast int, wydaje się, że przez długi czas wypisuje> 0 liczb. Dlaczego nie pojawia się ten problem po 63 iteracjach?
DeaIss

1
„Nie oblicza poprawnie” jest nieprawidłową charakterystyką. Obliczenia są poprawne, zgodnie ze specyfikacją języka Java, które powinno się wydarzyć. Prawdziwym problemem jest to, że wyniku (idealnego) obliczenia nie można przedstawić jako int.
Stephen C

@oOTesterOo - ponieważ longmoże reprezentować większe liczby niż intmoże.
Stephen C

Długi ma większy zasięg. Typ BigInteger akceptuje dowolną wartość / długość, którą może przydzielić maszyna JVM.
Bruno Volpato,

Założyłem, że wartość int przepełnia się po 31 iteracjach, ponieważ jest to 32-bitowa liczba o maksymalnym rozmiarze, a więc która z 64-bitów osiągnęłaby maksimum po 63? Dlaczego tak nie jest?
DeaIss

2

Do debugowania takich przypadków dobrze jest zmniejszyć liczbę iteracji w pętli. Użyj tego zamiast swojego while(true):

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

Możesz wtedy zobaczyć, że zaczyna się od 2 i podwaja wartość, aż spowoduje przepełnienie.


2

Do ilustracji użyję 8-bitowej liczby, ponieważ można ją szczegółowo opisać w niewielkiej przestrzeni. Liczby szesnastkowe zaczynają się od 0x, a liczby binarne od 0b.

Maksymalna wartość 8-bitowej liczby całkowitej bez znaku to 255 (0xFF lub 0b11111111). Jeśli dodasz 1, zwykle oczekujesz: 256 (0x100 lub 0b100000000). Ale ponieważ jest to zbyt wiele bitów (9), to jest powyżej maksimum, więc pierwsza część zostaje po prostu odrzucona, pozostawiając efektywnie 0 (0x (1) 00 lub 0b (1) 00000000, ale z 1 odrzuconym).

Więc kiedy program działa, otrzymujesz:

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

Największy literał dziesiętny typu intto 2147483648 (= 2 31 ). Wszystkie literały dziesiętne od 0 do 2147483647 mogą pojawić się wszędzie tam, gdzie może wystąpić literał int, ale literał 2147483648 może pojawić się tylko jako operand jednoargumentowego operatora negacji -.

Jeśli dodawanie liczb całkowitych przepełnia, wynikiem są najmniejsze bity sumy matematycznej, reprezentowane w jakimś wystarczająco dużym formacie uzupełnienia do dwóch. Jeśli wystąpi przepełnienie, znak wyniku nie jest taki sam, jak znak sumy matematycznej dwóch wartości operandów.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.