Hành vi trừ số nguyên không dấu có được xác định không?


100

Tôi đã gặp mã từ một người dường như tin rằng có sự cố khi trừ một số nguyên không dấu khỏi một số nguyên khác cùng loại khi kết quả sẽ là số âm. Vì vậy, mã như thế này sẽ không chính xác ngay cả khi nó hoạt động trên hầu hết các kiến ​​trúc.

unsigned int To, Tf;

To = getcounter();
while (1) {
    Tf = getcounter();
    if ((Tf-To) >= TIME_LIMIT) {
        break;
    } 
}

Đây là trích dẫn mơ hồ có liên quan duy nhất từ ​​tiêu chuẩn C mà tôi có thể tìm thấy.

Một phép tính liên quan đến các toán hạng không có dấu không bao giờ được vượt quá fl ow, bởi vì kết quả không thể được biểu diễn bằng kiểu số nguyên không dấu kết quả được giảm mô đun thành số lớn hơn một giá trị lớn nhất mà kiểu kết quả có thể biểu diễn.

Tôi cho rằng người ta có thể lấy câu trích dẫn đó có nghĩa là khi toán hạng bên phải lớn hơn, hoạt động được điều chỉnh để có ý nghĩa trong bối cảnh các số bị cắt ngắn theo mô-đun.

I E

0x0000 - 0x0001 == 0x 1 0000 - 0x0001 == 0xFFFF

trái ngược với việc sử dụng ngữ nghĩa đã ký phụ thuộc vào triển khai:

0x0000 - 0x0001 == (không dấu) (0 + -1) == (0xFFFF nhưng cũng 0xFFFE hoặc 0x8001)

Cách giải thích nào là đúng? Nó có được định nghĩa không?


3
Sự lựa chọn từ trong tiêu chuẩn là không may. Rằng nó "không bao giờ có thể tràn" có nghĩa là nó không phải là một tình huống lỗi. Sử dụng thuật ngữ trong tiêu chuẩn, thay vì làm tràn giá trị "kết thúc".
danorton

Câu trả lời:


107

Kết quả của một phép trừ tạo ra một số âm trong một kiểu không dấu được xác định rõ:

  1. [...] Một phép tính liên quan đến các toán hạng không có dấu không bao giờ có thể làm tràn, bởi vì kết quả không thể được biểu diễn bằng kiểu số nguyên không dấu kết quả được giảm mô-đun thành số lớn hơn một giá trị lớn nhất có thể được biểu diễn bằng kiểu kết quả. (ISO / IEC 9899: 1999 (E) §6.2.5 / 9)

Như bạn có thể thấy, (unsigned)0 - (unsigned)1bằng -1 modulo UINT_MAX + 1, hay nói cách khác, UINT_MAX.

Lưu ý rằng mặc dù nó nói rằng "Một phép tính liên quan đến các toán hạng không dấu không bao giờ có thể tràn", điều này có thể khiến bạn tin rằng nó chỉ áp dụng cho việc vượt quá giới hạn trên, nhưng điều này được trình bày như một động lực cho phần ràng buộc thực tế của câu: "a kết quả không thể được biểu diễn bằng kiểu số nguyên không dấu kết quả được giảm mô-đun theo số lớn hơn một giá trị lớn nhất có thể được biểu thị bằng kiểu kết quả. " Cụm từ này không bị giới hạn trong phạm vi tràn giới hạn trên của kiểu và áp dụng tương tự cho các giá trị quá thấp để được biểu diễn.


2
Cảm ơn bạn! Bây giờ tôi thấy cách giải thích mà tôi đã thiếu. Tôi nghĩ rằng họ có thể đã chọn một từ ngữ rõ ràng hơn.

4
Bây giờ tôi cảm thấy tốt hơn rất nhiều, biết rằng nếu bất kỳ phép cộng không dấu nào chuyển về 0 và gây ra tình trạng hỗn loạn, thì đó là do uintluôn được dự định để biểu diễn vòng toán học của các số nguyên 0thông qua UINT_MAXcác phép toán của mô đun cộng và nhân UINT_MAX+1, chứ không phải vì của tràn. Tuy nhiên, nó đặt ra câu hỏi tại sao, nếu các vòng là một kiểu dữ liệu cơ bản như vậy, thì ngôn ngữ này không cung cấp hỗ trợ chung hơn cho các vòng có kích thước khác.
Theodore Murdock

2
@TheodoreMurdock Tôi nghĩ câu trả lời cho câu hỏi đó rất đơn giản. Theo như tôi có thể nói, sự thật rằng đó là một hệ quả, không phải là một nguyên nhân. Yêu cầu thực sự là các kiểu không có dấu phải có tất cả các bit của chúng tham gia biểu diễn giá trị. Hành vi giống như chiếc nhẫn chảy tự nhiên từ đó. Nếu bạn muốn hành vi như vậy từ các kiểu khác, thì hãy làm số học của bạn, sau đó áp dụng mô-đun bắt buộc; sử dụng các toán tử cơ bản.
underscore_d

@underscore_d Tất nhiên ... rõ ràng tại sao họ đưa ra quyết định thiết kế. Thật là buồn cười khi họ viết thông số đại khái là "không có thừa / thiếu số học vì kiểu dữ liệu được chỉ định là một vòng", như thể lựa chọn thiết kế này có nghĩa là các lập trình viên không cần phải cẩn thận tránh thừa và thiếu -flow hoặc chương trình của họ bị lỗi một cách ngoạn mục.
Theodore Murdock

120

Khi bạn làm việc với các loại không dấu , số học mô-đun (còn được gọi là hành vi "quấn quanh" ) đang diễn ra. Để hiểu số học mô-đun này , chỉ cần xem các đồng hồ sau:

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

9 + 4 = 1 ( 13 mod 12 ), do đó theo chiều ngược lại là: 1 - 4 = 9 ( -3 mod 12 ). Nguyên tắc tương tự cũng được áp dụng khi làm việc với các kiểu không dấu. Nếu loại kết quảunsigned, thì số học mô-đun sẽ diễn ra.


Bây giờ hãy xem các hoạt động sau đây lưu trữ kết quả dưới dạng unsigned int:

unsigned int five = 5, seven = 7;
unsigned int a = five - seven;      // a = (-2 % 2^32) = 4294967294 

int one = 1, six = 6;
unsigned int b = one - six;         // b = (-5 % 2^32) = 4294967291

Khi bạn muốn chắc chắn rằng kết quả là có signed, hãy lưu trữ nó vào signedbiến hoặc ép kiểu sang signed. Khi bạn muốn nhận được sự khác biệt giữa các số và đảm bảo rằng số học mô-đun sẽ không được áp dụng, thì bạn nên cân nhắc sử dụng abs()hàm được định nghĩa trong stdlib.h:

int c = five - seven;       // c = -2
int d = abs(five - seven);  // d =  2

Hãy rất cẩn thận, đặc biệt là trong khi viết các điều kiện, bởi vì:

if (abs(five - seven) < seven)  // = if (2 < 7)
    // ...

if (five - seven < -1)          // = if (-2 < -1)
    // ...

if (one - six < 1)              // = if (-5 < 1)
    // ...

if ((int)(five - seven) < 1)    // = if (-2 < 1)
    // ...

nhưng

if (five - seven < 1)   // = if ((unsigned int)-2 < 1) = if (4294967294 < 1)
    // ...

if (one - six < five)   // = if ((unsigned int)-5 < 5) = if (4294967291 < 5)
    // ...

4
Một điều tuyệt vời với đồng hồ, mặc dù bằng chứng sẽ khiến điều này trở thành câu trả lời chính xác. Tiền đề của câu hỏi đã bao gồm khẳng định rằng tất cả những điều này có thể đúng.
Các cuộc đua ánh sáng trong quỹ đạo

5
@LightnessRacesinOrbit: Cảm ơn bạn. Tôi viết nó bởi vì tôi nghĩ rằng ai đó có thể thấy nó rất hữu ích. Tôi đồng ý, đó không phải là một câu trả lời đầy đủ.
LihO

4
Đường dây int d = abs(five - seven);không tốt. Đầu tiên five - sevenlà tính toán: thăng hạng để lại các loại toán hạng unsigned int, kết quả là modulo được tính toán (UINT_MAX+1)và đánh giá là UINT_MAX-1. Sau đó, giá trị này là tham số thực tế abs, đây là một tin xấu. abs(int)gây ra hành vi không xác định khi truyền đối số, vì nó không nằm trong phạm vi và abs(long long)có thể giữ giá trị, nhưng hành vi không xác định xảy ra khi giá trị trả về bị ép buộc intkhởi tạo d.
Ben Voigt

1
@LihO: Toán tử duy nhất trong C ++ nhạy cảm với ngữ cảnh và hoạt động khác nhau tùy thuộc vào cách kết quả của nó được sử dụng là một toán tử chuyển đổi tùy chỉnh operator T(). Phép cộng trong hai biểu thức chúng ta đang thảo luận được thực hiện theo kiểu unsigned int, dựa trên các kiểu toán hạng. Kết quả của phép cộng là unsigned int. Sau đó, kết quả đó được chuyển đổi hoàn toàn thành kiểu cần thiết trong ngữ cảnh, một chuyển đổi không thành công vì giá trị không thể đại diện trong kiểu mới.
Ben Voigt

1
@LihO: Nó có thể giúp đỡ để nghĩ về double x = 2/3;vsdouble y = 2.0/3;
Ben Voigt

5

Vâng, cách giải thích đầu tiên là đúng. Tuy nhiên, lý luận của bạn về "ngữ nghĩa ký hiệu" trong ngữ cảnh này là sai.

Một lần nữa, cách giải thích đầu tiên của bạn là đúng. Số học không dấu tuân theo các quy tắc của số học modulo, có nghĩa là 0x0000 - 0x0001đánh giá 0xFFFFcho các loại không dấu 32 bit.

Tuy nhiên, cách giải thích thứ hai (cách giải thích dựa trên "ngữ nghĩa ký hiệu") cũng được yêu cầu để tạo ra kết quả tương tự. Tức là ngay cả khi bạn đánh giá 0 - 1trong miền của kiểu có dấu và nhận được -1dưới dạng kết quả trung gian, điều này -1vẫn được yêu cầu tạo ra 0xFFFFkhi sau này nó được chuyển đổi sang kiểu không dấu. Ngay cả khi một số nền tảng sử dụng một biểu diễn ngoại lai cho các số nguyên có dấu (phần bù của 1, độ lớn có dấu), nền tảng này vẫn được yêu cầu áp dụng các quy tắc của số học modulo khi chuyển đổi các giá trị số nguyên có dấu thành không có dấu.

Ví dụ, đánh giá này

signed int a = 0, b = 1;
unsigned int c = a - b;

vẫn được đảm bảo để sản xuất UINT_MAXtrong c, ngay cả khi nền tảng này được sử dụng một đại diện độc đáo cho các số nguyên ký kết.


4
Tôi nghĩ ý bạn là loại 16 bit không dấu, không phải 32 bit.
xioxox

4

Với các số không có dấu của kiểu unsigned inthoặc lớn hơn, trong trường hợp không có chuyển đổi kiểu, a-bđược định nghĩa là mang lại số không có dấu, khi được thêm vào b, sẽ mang lại a. Việc chuyển đổi một số âm thành không có dấu được định nghĩa là mang lại số mà khi được thêm vào số ban đầu được đảo ngược dấu hiệu, sẽ mang lại kết quả bằng 0 (vì vậy chuyển -5 thành không dấu sẽ mang lại giá trị mà khi được thêm vào 5, sẽ mang lại giá trị 0) .

Lưu ý rằng các số không có dấu nhỏ hơn unsigned intcó thể được đưa lên loại inttrước phép trừ, hành vi của a-bsẽ phụ thuộc vào kích thước của int.

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.