30/09/2018, 17:05

Tại sao n đang thành dương sao đó chyển thành âm với phép toán `n<<=1`

Đây là bài: “chuyển từ thập phân sang nhị phân”. Với đoạn code dưới, n sẽ chuyển từ số dương thành số âm.

#include <stdio.h>

int main()
{
    int n = 2;
    for (int i = 0, temp = (sizeof(n) * 8); i < temp; i++, n <<= 1)
        if (n >= 0)
            printf("0");
        else
            printf("1");
    printf("
");
    return 0;
}

Kết quả chương trình sẽ trả ra

00000000000000000000000000000010

Cho em hỏi tại sao n đang thành dương sao đó chyển thành âm. Cái này có phải do miền giá trị k ạ?
n từ âm chuyển thành dương thì e lại k biết.

Gió viết 19:12 ngày 30/09/2018

Để đơn giản hơn ta dùng kiểu char8 bit.

  • Lúc Đầu giả sử các bit đang có là abcdefgh

Khi dịch trái 1 bit các bit thừa bị cắt đi và thêm 1 bit 0 vào sau
Vòng lặp for diễn ra như sau:

#i *signed bit
 
#0 abcdefgh
#1 bcdefgh0
#2 cdefgh00
#3 defgh000
#4 efgh0000
#5 fgh00000
#6 gh000000
#7 h0000000

Kiểu intchar có bit cao nhất bit đầu tiên chứa dấu của số đó:

Như vậy bít cao nhất sẽ theo thứ tự các bit đầu tiên là: abcdefgh

Byn viết 19:09 ngày 30/09/2018

Vic viết 19:18 ngày 30/09/2018

Lỗi này có tên là Integer overflow (tràn số nguyên). Cụ thể như sau:
Vòng lặp của bạn sẽ lặp 32 lần (0 -> 31). Như vậy đoạn lệnh trên tương đương trên sẽ tương đương với 2^32 = 4.294.967.296, gtrị này lớn hơn khả năng chứa của kiểu int, giới hạn của kiểu int là [-2.147.483.648…2.147.483.647], lúc này biến n sẽ bị tràn và bị quay về gtrị âm dẫn đến kết quả như trên.

Hoan Sò viết 19:15 ngày 30/09/2018

Nhưng nó lại từ kết quả âm trở về giá trị dương song lại tiếp tục bạn à

Vic viết 19:18 ngày 30/09/2018

Như mình đã nói. Ở đây, khi nó lặp đến vòng:
Lần thứ 29: n = 1.073.741.824
Lần thứ 30: n = -2.147.483.648 *Chú ý lần này: Nếu ko bị tràn thì gtrị thực của nó là là 2.147.483.648 nhưng vì bị tràn quá giới hạn của nó là 2.147.483.647 nên dẫn đến nó sẽ tràn sang bị quay vòng về -2.147.483.648.
Lần thứ 31: Tương tự câu tl trên nhưng nó ngược lại. Lúc này sau khi nhân thêm với 2, gtrị vượt qua kích thước 4 bytes và các bit của số này lúc này là 100…00, mà kích thước của n là 4 bytes nên nó chỉ nhận đủ 4 bytes dẫn đến n chỉ nhận những số 0 phía sau của số vừa nhân kia, nghĩa là lúc này n = 0. Do điều kiện là n >= 0 sẽ in ra 0 nên kết quả sẽ hiển thị như bạn paste ở trên.

Hoan Sò viết 19:12 ngày 30/09/2018

Cảm ơn bạn vì những chưa sẻ rất bổ ích đây. Nhưng mình vẫn chưa hiểu chỗ này mong bạn giải thích hô t
Nếu thay n=166 thì khi
i=23: n= 1,392,508,928
i=24: n= -1,509,949,440
i=25: n= 1,275,068,416

Theo như cách bạn nói thì mình vẫn k hiểu là n đang âm nếu nhân 2 vào thì vượt quá giới hạn kiểu int
nhưng n lại ra số dương là sao?

Vic viết 19:11 ngày 30/09/2018

Trc khi tl thì mình có nhận xét ntn: Câu hỏi này khá hay. Thực tế, rất ít ng biết và để ý đến lỗi này nếu ko để ý kỹ, hay ko hiểu rõ bản chất. Trc đã có một số ng hỏi nhưng mình bận chưa tl đc. Hôm nay rảnh rỗi nên tl luôn cho những ai chưa biết. Mặc dù câu hỏi này ngắn nhưng để hiểu rõ vđề này thì ko có cách tl nào có thể ngắn để hiểu rõ chi tiết đc. Vđề này ko thuộc phạm vi lập trình mà nó thuộc phạm vi khai thác lỗ hổng phần mềm rồi.

Để gthích rõ, mình sẽ nói cụ thể hơn. Máy tính thực ra nó ko dử dụng hệ thập phân (Decan - 10) như con ng hay sử dụng mà thay vào đó nó sử dụng hệ thập lục (Hexan - 16). Chính vì vậy nó ko có quy tắc dấu âm, dấu dương vậy nên miền giá trị của nó cũng khác so với con ng.
*Chú ý: Mình sử dụng cả hệ thập lục biểu diễn số cho dễ hiểu vì nó đủ số byte sẽ dễ hình dung hơn. Ví dụ số int 932.740.237 kích thước 4 bytes nên biểu diễn số này là 4 cặp số 37.98.7C.8Dh.

Mình sẽ lấy ví dụ nhẹ nhàng hơn cho bạn hiểu, nv dễ dàng hơn là cứ theo đoạn code kia.
Mình lấy ví dụ về kiểu char có kích thước 1 byte thay vì kiểu int 4 byte kia.
Thông tin sơ lược về kiểu này nsau:
Tên kiểu dữ liệu: char
Kích thước: 1 Byte
Miền giá trị: -128…127 (-128…-1: Biểu diễn cho miền gtrị âm, 0…127: Biểu diễn cho miền gtrị dương).

Như mình nói ở trên, thay vì sử dụng số thập phân máy sẽ sử dụng phương pháp bù số 2 để chuyển gtrị sang hệ thập lục. Nếu bạn học qua môn Kiến trúc máy tính và hệ điều hành rồi bạn sẽ hiểu rõ phương pháp này, hoặc bạn có thể tìm hiểu phương pháp này trên Google*.
Một phương pháp nữa là sử dụng cách sau cho đơn giản hoá việc chuyển đổi số âm giữa hai hệ số theo kích thước kiểu dữ liệu:
X’ = 2^8n - abs(X) // #1.Miền âm sang miền dương
X = -(2^8n - X’) // #2. Miền dương sang miền âm
Trong đó n là kích thước của kiểu dữ liệu, X là số thuộc miền âm muốn chuyển, X’ là số thuộc miền dương muốn chuyển.
Công thức này mình chỉ tự nghĩ ra để đơn giản hoá đi thôi.

Ví dụ:
n = sizeof(char) = 1
N = -12 -> N’ = 2^8 - |-12| = 244 (F4h)
N’ = 235 -> N = -(2^8 - 235) = -21 (EBh)

Vậy nên ta có miền gtrị tương ứng của kiểu dữ liệu char theo máy tính như sau: 80h…FFh (Miền âm, -128…-1), 0…7F (Miền dương, 0…127).

Bây giờ ta có một ví dụ về tràn số mà bạn đang chưa hiểu ở trên. Ta có đoạn code ví dụ sau:
{
char N = 123;
*N = 2;
cout << N << endl;
*N = 13;
cout << N << endl;
}
Ở dòng N *= 2, sau khi lệnh này thực thi kết quả sẽ bị tràn số vì 123 * 2 = 246 (F6h). Do bị vượt quá miền gtrị dương [0…7Fh] sang miền âm [80h…FFh]. Dựa vào #1 thì F6 = -10. Vậy nên thay vì N = 246 thì thực tế N = -10.
Vấn đề 1 đc giải quyết: Từ một số nguyên dương bị chuyển thành số nguyên âm
Tiếp đến dòng N *= 13, sau khi lệnh này thực thi kết quả cũng sẽ bị tràn số vì N lúc này = 10 -> -10 * 13 = -130 (7Eh) vượt qua miền gtrị âm [80F…FFh] sang miền gtrị dương là [0…7Fh]. Dựa vào #1 nên thực tế N = 126.
Vấn đề 2 đc giải quyết: Từ một số nguyên âm bị chuyển thành số nguyên dương


Áp dụng với kquả bạn đưa:
n = 166.
i=23: n= 1,392,508,928;
i=24: n= -1,509,949,440
i=25: n= 1,275,068,416

n là kiểu int, kích thước 4 byte miền gtrị [-2.147.483.648…-1, 0…2.147.483.647] tương đương miền gtrị máy sử dụng là [0…FFFFFFFFh]. Miền âm [8000.0000h…FFFF.FFFFh], miền dương [0…7FFF.FFFFh].

i= 23 -> 166 * 2^23 = 1,392,508,928 (5300.0000h) vẫn thuộc miền dương nên ko bị tràn.
i = 24 -> 166 * 2^24 = 2.785.017.856 (A600.0000h) tràn khỏi miền dương sang miền âm. Dựa vào #2 thì: N’ = -(2*32 - |2.785.017.856|) = -1.509.949.440.
i = 25 -> 166 * 2^25 = 5.570.035.712 (1.4C00.0000h) một số kích thước 33 bit, tràn hẳn ra khỏi kiểu dữ liệu int giới hạn 32 bit nhưng biến n lúc này chỉ nhận đủ 32 bit nên 1.4C00.0000h sẽ chỉ còn là 4C00.0000h = 1.275.068.416.
Đó là lý do xảy ra nguyên nhân như trên.
Qua đây hi vọng những ai chưa biết hiểu để sau lập trình hình dung, xử lý kiểu dữ liệu tốt hơn. Những lỗi này rất nguy hiểm và dễ bị khai khác gây hậu quả nghiêm trọng!

Bài liên quan
0