Tại sao i = i + i cho tôi 0?


96

Tôi có một chương trình đơn giản:

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

Khi tôi chạy chương trình này, tất cả tôi thấy là 0cho isản lượng của tôi. Tôi đã mong đợi vòng lần đầu tiên chúng tôi sẽ có i = 1 + 1, tiếp theo i = 2 + 2, tiếp theo là i = 4 + 4v.v.

Điều này có phải do thực tế là ngay sau khi chúng tôi cố gắng khai báo ilại ở phía bên trái, giá trị của nó được đặt lại thành 0không?

Nếu ai đó có thể chỉ cho tôi những chi tiết tốt hơn về điều này thì điều đó thật tuyệt.

Thay đổi intthành longvà có vẻ như nó đang in các số như mong đợi. Tôi ngạc nhiên về tốc độ nó đạt đến giá trị 32-bit tối đa!

Câu trả lời:


168

Vấn đề là do tràn số nguyên.

Trong số học 32 bit hai phần bù:

ithực sự bắt đầu có giá trị lũy thừa của hai, nhưng sau đó các hành vi tràn sẽ bắt đầu khi bạn đạt đến 2 30 :

2 30 + 2 30 = -2 31

-2 31 + -2 31 = 0

... trong intsố học, vì nó về cơ bản là mod số học 2 ^ 32.


28
Bạn có thể mở rộng câu trả lời của mình một chút không?
DeaIss

17
@oOTesterOo Nó bắt đầu in ấn 2, 4 vv nhưng nó rất nhanh chóng đạt giá trị lớn nhất của số nguyên và nó "kết thúc tốt đẹp xung quanh" để số âm, một khi nó đạt đến số không nó sẽ nằm ở số không vĩnh viễn
Richard Tingle

52
Câu trả lời này là thậm chí không đầy đủ (nó thậm chí không đề cập đến rằng giá trị sẽ không được 0trên vài lần lặp đầu tiên, nhưng tốc độ của đầu ra được che khuất mà thực tế từ OP). Tại sao nó được chấp nhận?
Lightness Races in Orbit

16
Có lẽ nó đã được chấp nhận vì nó được OP coi là hữu ích.
Joe

4
@LightnessRacesinOrbit Mặc dù nó không trực tiếp giải quyết các vấn đề mà OP đưa ra trong câu hỏi của họ, nhưng câu trả lời cung cấp đủ thông tin để một lập trình viên giỏi có thể suy ra điều gì đang xảy ra.
Kevin

334

Giới thiệu

Vấn đề là tràn số nguyên. Nếu nó bị tràn, nó sẽ quay trở lại giá trị nhỏ nhất và tiếp tục từ đó. Nếu nó chảy dưới mức, nó sẽ quay trở lại giá trị lớn nhất và tiếp tục từ đó. Hình ảnh dưới đây là của một Odometer. Tôi sử dụng điều này để giải thích tràn. Đó là một sự tràn cơ học nhưng vẫn là một ví dụ điển hình.

Trong một Odometer, max digit = 9vì vậy, vượt quá phương tiện tối đa 9 + 1, mang lại và cho một 0; Tuy nhiên, không có chữ số cao hơn để thay đổi thành a 1, vì vậy bộ đếm đặt lại thành zero. Bạn có ý tưởng - "tràn số nguyên" hiện ra trong đầu.

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

Chữ thập phân lớn nhất của kiểu int là 2147483647 (2 31 -1). Tất cả các ký tự thập phân từ 0 đến 2147483647 có thể xuất hiện ở bất kỳ nơi nào mà một ký tự int có thể xuất hiện, nhưng ký tự 2147483648 có thể chỉ xuất hiện dưới dạng toán hạng của toán tử phủ định một ngôi -.

Nếu một phép cộng số nguyên bị tràn, thì kết quả là các bit bậc thấp của tổng toán học như được biểu diễn trong một số định dạng phần bù đủ lớn của hai. Nếu xảy ra tràn, thì dấu của kết quả không giống như dấu của tổng toán học của hai giá trị toán hạng.

Do đó, 2147483647 + 1tràn và bao quanh thành -2147483648. Do đó int i=2147483647 + 1sẽ bị tràn, không bằng 2147483648. Ngoài ra, bạn nói "nó luôn in 0". Nó không, bởi vì http://ideone.com/WHrQIW . Dưới đây, 8 con số này hiển thị điểm mà nó xoay và tràn. Sau đó, nó bắt đầu in các số 0. Ngoài ra, đừng ngạc nhiên khi nó tính toán nhanh như thế nào, máy móc ngày nay rất nhanh.

268435456
536870912
1073741824
-2147483648
0
0
0
0

Tại sao tràn số nguyên "quấn quanh"

PDF gốc


17
Tôi đã thêm hoạt ảnh cho "Pacman" với mục đích tượng trưng nhưng nó cũng đóng vai trò là một hình ảnh tuyệt vời về cách người ta sẽ thấy 'tràn số nguyên'.
Ali Gajani

9
Đây là câu trả lời yêu thích của tôi trên trang web này mọi lúc.
Lee White

2
Bạn dường như đã bỏ lỡ rằng đây là một chuỗi nhân đôi, không thêm một.
Paŭlo Ebermann

2
Tôi nghĩ rằng phim hoạt hình pacman có câu trả lời này nhiều lượt ủng hộ hơn câu trả lời được chấp nhận. Có một ủng hộ khác cho tôi - đó là một trong những trò chơi yêu thích của tôi!
Husman

3
Đối với những ai đã không nhận được biểu tượng: en.wikipedia.org/wiki/Kill_screen#Pac-Man
wei2912

46

Không, nó không chỉ in các số không.

Thay đổi nó thành này và bạn sẽ thấy điều gì sẽ xảy ra.

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

Điều gì xảy ra được gọi là tràn.


61
Cách thú vị để viết một vòng lặp for :)
Bernhard

17
@Bernhard Nó có thể là để giữ cấu trúc của chương trình của OP.
Taemyr

4
@Taemyr Có lẽ, nhưng sau đó ông có thể đã thay thế truevới i<10000:)
Bernhard

7
Tôi chỉ muốn thêm một vài tuyên bố; mà không cần xóa / thay đổi bất kỳ câu lệnh nào. Tôi ngạc nhiên khi nó thu hút sự chú ý rộng rãi như vậy.
peter.petrov

18
Bạn có thể sử dụng các nhà điều hành giấu while(k --> 0)thông tục có tên là "khi kđi vào 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);
        }
    }

đưa ra:

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

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

4
Ừm, không, nó chỉ hoạt động cho chuỗi cụ thể này về 0. Hãy thử bắt đầu với 3.
Paŭlo Ebermann.

4

Vì tôi không có đủ danh tiếng nên không thể đăng hình ảnh đầu ra cho cùng một chương trình trong C với đầu ra được kiểm soát, bạn có thể tự thử và thấy rằng nó thực sự in 32 lần và sau đó như đã giải thích do tràn i = 1073741824 + 1073741824 thay đổi thành -2147483648 và một bổ sung nữa nằm ngoài phạm vi int và chuyển thành 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
Chương trình này, trong C, thực sự kích hoạt hành vi không xác định trong mỗi lần thực thi, cho phép trình biên dịch thay thế toàn bộ chương trình bằng bất kỳ thứ gì (ngay cả system("deltree C:")khi bạn đang sử dụng DOS / Windows). Tràn số nguyên có dấu là hành vi không xác định trong C / C ++, không giống như Java. Hãy rất cẩn thận khi sử dụng loại cấu trúc này.
filcab

@filcab: "thay thế toàn bộ chương trình bằng bất cứ thứ gì" bạn đang nói về cái gì vậy. Tôi đã chạy chương trình này trên Visual studio 2012 và nó chạy hoàn toàn tốt cho cả hai signed and unsignedsố nguyên mà không có bất kỳ hành vi không xác định
Kaify

3
@Kaify: Làm việc tốt là một hành vi không xác định hoàn toàn hợp lệ. Tuy nhiên, hãy tưởng tượng rằng mã đã làm i += icho hơn 32 lần lặp, sau đó có if (i > 0). Trình biên dịch có thể tối ưu hóa điều đó if(true)vì nếu chúng ta luôn thêm các số dương, isẽ luôn lớn hơn 0. Nó cũng có thể để nguyên điều kiện, nơi nó sẽ không được thực thi, do tràn biểu thị ở đây. Vì trình biên dịch có thể tạo ra hai chương trình hợp lệ như nhau từ mã đó, nên đó là hành vi không xác định.
3Doubloons

1
@Kaify: đó không phải là phân tích từ vựng, đó là trình biên dịch biên dịch mã của bạn và tuân theo tiêu chuẩn, có thể thực hiện các tối ưu hóa "kỳ lạ". Giống như vòng lặp mà 3Doubloons đã nói về. Chỉ vì các trình biên dịch mà bạn cố gắng dường như luôn làm được điều gì đó, điều đó không có nghĩa là tiêu chuẩn đảm bảo chương trình của bạn sẽ luôn chạy theo cùng một cách. Bạn đã có hành vi không xác định, một số mã có thể đã bị loại bỏ vì không có cách nào để đạt được điều đó (UB đảm bảo điều đó). Các bài đăng này từ blog llvm (và các liên kết trong đó) có thêm thông tin: blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
filcab

2
@Kaify: Xin lỗi vì bạn đã không đặt nó, nhưng hoàn toàn sai khi nói "giữ bí mật", đặc biệt khi đó là kết quả thứ hai, trên Google, cho "hành vi không xác định", là thuật ngữ cụ thể tôi đã sử dụng cho những gì đang được kích hoạt .
filcab

4

Giá trị của iđược lưu trong bộ nhớ bằng cách sử dụng một lượng cố định các chữ số nhị phân. Khi một số cần nhiều chữ số hơn số sẵn có, chỉ các chữ số thấp nhất được lưu trữ (các chữ số cao nhất bị mất).

Cộng ivới chính nó cũng giống như nhân ivới hai. Giống như việc nhân một số với mười trong ký hiệu thập phân có thể được thực hiện bằng cách trượt từng chữ số sang trái và đặt số 0 ở bên phải, nhân một số với hai trong ký hiệu nhị phân có thể được thực hiện theo cách tương tự. Điều này thêm một chữ số ở bên phải, vì vậy một chữ số bị mất ở bên trái.

Ở đây giá trị bắt đầu là 1, vì vậy nếu chúng ta sử dụng 8 chữ số để lưu trữ i(ví dụ),

  • sau 0 lần lặp, giá trị là 00000001
  • sau 1 lần lặp, giá trị là 00000010
  • sau 2 lần lặp, giá trị là 00000100

và cứ tiếp tục như vậy cho đến bước cuối cùng khác không

  • sau 7 lần lặp, giá trị là 10000000
  • sau 8 lần lặp, giá trị là 00000000

Bất kể có bao nhiêu chữ số nhị phân được phân bổ để lưu trữ số và bất kể giá trị bắt đầu là bao nhiêu, cuối cùng tất cả các chữ số sẽ bị mất khi chúng bị đẩy sang trái. Sau thời điểm đó, tiếp tục nhân đôi số sẽ không thay đổi số - nó vẫn được biểu thị bằng tất cả các số 0.


3

Nó đúng, nhưng sau 31 lần lặp, 1073741824 + 1073741824 không tính đúng và sau đó chỉ in ra 0.

Bạn có thể cấu trúc lại để sử dụng BigInteger, vì vậy vòng lặp vô hạn của bạn sẽ hoạt động chính xác.

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

Nếu tôi sử dụng long thay vì int, nó có vẻ là in> 0 số trong một thời gian dài. Tại sao nó không gặp sự cố này sau 63 lần lặp?
DeaIss

1
"Không tính toán chính xác" là một đặc điểm không chính xác. Việc tính toán đúng theo những gì Java nói sẽ xảy ra. Vấn đề thực sự là kết quả của phép tính (lý tưởng) không thể được biểu diễn dưới dạng một int.
Stephen C

@oOTesterOo - vì longcó thể đại diện cho số lớn hơn intcó thể.
Stephen C

Long có phạm vi lớn hơn. Kiểu BigInteger chấp nhận bất kỳ giá trị / độ dài nào mà JVM của bạn có thể phân bổ.
Bruno Volpato

Tôi giả định rằng int bị tràn sau 31 lần lặp vì số có kích thước tối đa 32-bit và bao lâu thì số 64-bit sẽ đạt mức tối đa sau 63? Tại sao đó không phải là trường hợp?
DeaIss

2

Để gỡ lỗi các trường hợp như vậy, tốt hơn là giảm số lần lặp lại trong vòng lặp. Sử dụng cái này thay vì của bạn while(true):

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

Sau đó, bạn có thể thấy rằng nó bắt đầu bằng 2 và đang tăng gấp đôi giá trị cho đến khi nó gây tràn.


2

Tôi sẽ sử dụng số 8 bit để minh họa vì nó có thể hoàn toàn chi tiết trong một không gian ngắn. Số hex bắt đầu bằng 0x, trong khi số nhị phân bắt đầu bằng 0b.

Giá trị tối đa cho một số nguyên không dấu 8 bit là 255 (0xFF hoặc 0b11111111). Nếu bạn thêm 1, bạn thường mong đợi nhận được: 256 (0x100 hoặc 0b100000000). Nhưng vì đó là quá nhiều bit (9), vượt quá mức tối đa, vì vậy phần đầu tiên chỉ bị loại bỏ, để lại cho bạn 0 hiệu quả (0x (1) 00 hoặc 0b (1) 00000000, nhưng với 1 bị giảm).

Vì vậy, khi chương trình của bạn chạy, bạn nhận được:

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

Chữ thập phân lớn nhất của loại int2147483648 (= 2 31 ). Tất cả các ký tự thập phân từ 0 đến 2147483647 có thể xuất hiện ở bất kỳ nơi nào mà một ký tự int có thể xuất hiện, nhưng ký tự 2147483648 có thể chỉ xuất hiện dưới dạng toán hạng của toán tử phủ định một ngôi -.

Nếu một phép cộng số nguyên bị tràn, thì kết quả là các bit bậc thấp của tổng toán học như được biểu diễn trong một số định dạng phần bù đủ lớn của hai. Nếu xảy ra tràn, thì dấu của kết quả không giống như dấu của tổng toán học của hai giá trị toán hạng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.