Một số các câu trả lời ở đây đề cập đến các quy tắc xúc tiến đáng ngạc nhiên giữa các giá trị ký kết và unsigned, nhưng điều đó có vẻ giống như một vấn đề liên quan đến pha trộn các giá trị ký kết và unsigned, và không nhất thiết giải thích tại sao có chữ ký biến sẽ được ưa thích hơn unsigned bên ngoài trộn kịch bản.
Theo kinh nghiệm của tôi, ngoài các quy tắc so sánh và thăng hạng hỗn hợp, có hai lý do chính khiến các giá trị không có dấu là nam châm lỗi như sau.
Các giá trị không dấu có sự gián đoạn bằng 0, giá trị phổ biến nhất trong lập trình
Cả số nguyên không dấu và có dấu đều có sự gián đoạn ở các giá trị nhỏ nhất và lớn nhất của chúng, nơi chúng quấn quanh (không dấu) hoặc gây ra hành vi không xác định (có dấu). Đối với unsigned
những điểm này là 0 và UINT_MAX
. Vì int
họ đang ở INT_MIN
và INT_MAX
. Các giá trị điển hình của INT_MIN
và INT_MAX
trên hệ thống có int
giá trị 4 byte là -2^31
và 2^31-1
và trên hệ thống như vậy UINT_MAX
thường là2^32-1
.
Vấn đề chính gây ra lỗi unsigned
không áp dụng cho int
nó là nó có sự gián đoạn ở mức 0 . Tất nhiên, số không là một giá trị rất phổ biến trong các chương trình, cùng với các giá trị nhỏ khác như 1,2,3. Việc cộng và trừ các giá trị nhỏ, đặc biệt là 1, trong các cấu trúc khác nhau, và nếu bạn trừ bất kỳ unsigned
giá trị nào khỏi một giá trị và nó xảy ra bằng 0, bạn chỉ nhận được một giá trị dương lớn và một lỗi gần như nhất định.
Hãy xem xét mã lặp lại trên tất cả các giá trị trong một vectơ theo chỉ mục ngoại trừ 0,5 cuối cùng :
for (size_t i = 0; i < v.size() - 1; i++) {
Điều này hoạt động tốt cho đến một ngày bạn vượt qua một vector trống. Thay vì thực hiện 0 lần lặp, bạn nhận được v.size() - 1 == a giant number
1 và bạn sẽ thực hiện 4 tỷ lần lặp và gần như có lỗ hổng tràn bộ đệm.
Bạn cần viết nó như thế này:
for (size_t i = 0; i + 1 < v.size(); i++) {
Vì vậy, nó có thể được "sửa chữa" trong trường hợp này, nhưng chỉ bằng cách suy nghĩ cẩn thận về bản chất không dấu của size_t
. Đôi khi bạn không thể áp dụng cách khắc phục ở trên bởi vì thay vì một hằng số, bạn có một số bù biến số mà bạn muốn áp dụng, có thể là dương hoặc âm: vì vậy, "bên" nào của phép so sánh mà bạn cần đặt nó phụ thuộc vào độ dấu - bây giờ mã thực sự trở nên lộn xộn.
Có một vấn đề tương tự với mã cố gắng lặp xuống và bao gồm cả số không. Một cái gì đó như while (index-- > 0)
hoạt động tốt, nhưng tương đương rõ ràng while (--index >= 0)
sẽ không bao giờ kết thúc đối với một giá trị không dấu. Trình biên dịch của bạn có thể cảnh báo bạn khi phía bên tay phải là chữ không, nhưng chắc chắn không nếu nó là một giá trị xác định tại thời gian chạy.
Đối điểm
Một số người có thể tranh luận rằng các giá trị có dấu cũng có hai điểm không liên tục, vậy tại sao lại chọn không có dấu? Sự khác biệt là cả hai điểm gián đoạn đều rất (tối đa) cách xa 0. Tôi thực sự coi đây là một vấn đề riêng biệt của "tràn", cả giá trị có dấu và không dấu có thể tràn ở các giá trị rất lớn. Trong nhiều trường hợp, việc làm tràn là không thể xảy ra do các ràng buộc về phạm vi giá trị có thể có và việc tràn nhiều giá trị 64 bit có thể là không thể thực hiện được). Ngay cả khi có thể, khả năng xảy ra lỗi liên quan đến tràn thường rất nhỏ so với lỗi "ở mức không" và lỗi tràn cũng xảy ra đối với các giá trị chưa được đánh dấu . Vì vậy, không dấu kết hợp điều tồi tệ nhất của cả hai thế giới: có khả năng tràn các giá trị cường độ rất lớn và sự gián đoạn ở mức 0. Đã ký chỉ có trước đây.
Nhiều người sẽ tranh luận "bạn thua một chút" với trái dấu. Điều này thường đúng - nhưng không phải lúc nào cũng đúng (nếu bạn cần biểu thị sự khác biệt giữa các giá trị không có dấu, dù sao thì bạn cũng sẽ mất bit đó: rất nhiều thứ 32 bit được giới hạn ở 2 GiB, hoặc bạn sẽ có một vùng màu xám kỳ lạ khi nói một tệp có thể là 4 GiB, nhưng bạn không thể sử dụng một số API nhất định trên nửa sau 2 GiB).
Ngay cả trong những trường hợp không ký tên cũng mua cho bạn một chút: nó không mua cho bạn nhiều: nếu bạn phải hỗ trợ hơn 2 tỷ "thứ", có lẽ bạn sẽ sớm phải hỗ trợ hơn 4 tỷ.
Về mặt logic, các giá trị không dấu là một tập hợp con của các giá trị có dấu
Về mặt toán học, các giá trị không dấu (số nguyên không âm) là một tập hợp con của các số nguyên có dấu (được gọi là _integers). 2 . Tuy nhiên, có chữ ký giá trị tự nhiên bật ra khỏi các hoạt động hoàn toàn vào unsigned giá trị, chẳng hạn như phép trừ. Chúng tôi có thể nói rằng các giá trị chưa được đánh dấu không bị đóng dưới phép trừ. Điều này cũng không đúng với các giá trị đã ký.
Bạn muốn tìm "delta" giữa hai chỉ mục không dấu vào một tệp? Tốt hơn hết bạn nên thực hiện phép trừ theo đúng thứ tự, nếu không bạn sẽ nhận được câu trả lời sai. Tất nhiên, bạn thường cần kiểm tra thời gian chạy để xác định đúng thứ tự! Khi xử lý các giá trị không dấu dưới dạng số, bạn sẽ thường thấy rằng các giá trị có dấu (một cách hợp lý) luôn xuất hiện, vì vậy bạn cũng có thể bắt đầu với có dấu.
Đối điểm
Như đã đề cập trong chú thích (2) ở trên, các giá trị có dấu trong C ++ không thực sự là một tập hợp con của các giá trị không dấu có cùng kích thước, vì vậy các giá trị không dấu có thể đại diện cho cùng một số kết quả mà các giá trị có dấu có thể.
Đúng, nhưng phạm vi ít hữu ích hơn. Hãy xem xét phép trừ và các số không có dấu có phạm vi từ 0 đến 2N và các số có dấu với phạm vi từ -N đến N. Các phép trừ tùy ý dẫn đến kết quả trong phạm vi -2N đến 2N trong các trường hợp _ thứ và một trong hai loại số nguyên chỉ có thể biểu diễn một nửa của nó. Nó chỉ ra rằng vùng tập trung xung quanh 0 của -N đến N thường hữu ích hơn (chứa nhiều kết quả thực tế hơn trong mã thế giới thực) so với phạm vi từ 0 đến 2N. Hãy xem xét bất kỳ phân phối điển hình nào khác với phân phối đồng nhất (log, zipfian, bình thường, bất kỳ) và xem xét việc trừ các giá trị được chọn ngẫu nhiên khỏi phân phối đó: nhiều giá trị kết thúc bằng [-N, N] hơn [0, 2N] (thực sự, kết quả là phân phối luôn luôn có tâm ở vị trí không).
64-bit đóng cửa vì nhiều lý do để sử dụng các giá trị có dấu làm số
Tôi nghĩ rằng các đối số ở trên đã hấp dẫn đối với các giá trị 32 bit, nhưng các trường hợp tràn, ảnh hưởng đến cả có dấu và không dấu ở các ngưỡng khác nhau, làm xảy ra cho các giá trị 32-bit, vì "2 tỷ" là một con số đó có thể vượt qua bởi nhiều các đại lượng trừu tượng và vật lý (hàng tỷ đô la, hàng tỷ nano giây, mảng với hàng tỷ phần tử). Vì vậy, nếu ai đó đủ thuyết phục bằng cách tăng gấp đôi phạm vi tích cực cho các giá trị không dấu, họ có thể biến trường hợp tràn thành vấn đề và nó hơi ủng hộ không dấu.
Bên ngoài các miền chuyên biệt, giá trị 64-bit phần lớn loại bỏ mối quan tâm này. Các giá trị 64 bit đã ký có phạm vi cao hơn là 9.223.372.036.854.775.807 - hơn chín tạ . Đó là rất nhiều nano giây (giá trị khoảng 292 năm) và rất nhiều tiền. Nó cũng là một mảng lớn hơn bất kỳ máy tính nào có khả năng có RAM trong một không gian địa chỉ nhất quán trong một thời gian dài. Vì vậy, có lẽ 9 tạ tỷ là đủ cho tất cả mọi người (hiện tại)?
Khi nào sử dụng các giá trị không dấu
Lưu ý rằng hướng dẫn kiểu không cấm hoặc thậm chí không khuyến khích sử dụng các số không có dấu. Nó kết thúc bằng:
Không sử dụng kiểu không dấu chỉ để khẳng định rằng một biến không âm.
Thật vậy, có những cách sử dụng tốt cho các biến không dấu:
Khi bạn muốn coi một số lượng N-bit không phải là một số nguyên, mà chỉ đơn giản là một "túi bit". Ví dụ: dưới dạng bitmask hoặc bitmap, hoặc N giá trị boolean hoặc bất cứ thứ gì. Việc sử dụng này thường đi đôi với các loại chiều rộng cố định như uint32_t
và uint64_t
vì bạn thường muốn biết kích thước chính xác của biến. Một gợi ý rằng một biến đặc biệt xứng đáng điều trị này là bạn chỉ hoạt động trên nó với với Bitwise nhà khai thác như ~
, |
, &
, ^
, >>
và như vậy, chứ không phải với các phép tính số học như +
, -
, *
, /
, vv
Không có dấu là lý tưởng ở đây vì hành vi của các toán tử bitwise được xác định rõ và chuẩn hóa. Các giá trị đã ký có một số vấn đề, chẳng hạn như hành vi không xác định và không xác định khi dịch chuyển và biểu diễn không xác định.
Khi bạn thực sự muốn số học mô-đun. Đôi khi bạn thực sự muốn số học mô-đun 2 ^ N. Trong những trường hợp này, "tràn" là một tính năng, không phải lỗi. Các giá trị không dấu cung cấp cho bạn những gì bạn muốn ở đây vì chúng được xác định để sử dụng số học mô-đun. Các giá trị đã ký không thể được sử dụng (dễ dàng, hiệu quả) vì chúng có biểu diễn không xác định và tràn là không xác định.
0,5 Sau khi viết cái này, tôi nhận ra đây gần giống với ví dụ của Jarod mà tôi chưa từng thấy - và vì lý do chính đáng, đó là một ví dụ tốt!
1 Chúng ta đang nói đến size_t
ở đây nên thường là 2 ^ 32-1 trên hệ thống 32 bit hoặc 2 ^ 64-1 trên hệ thống 64 bit.
2 Trong C ++, đây không phải là trường hợp chính xác vì các giá trị không dấu chứa nhiều giá trị ở đầu trên hơn so với kiểu có dấu tương ứng, nhưng vấn đề cơ bản tồn tại là thao tác các giá trị không dấu có thể dẫn đến (một cách hợp lý) các giá trị có dấu, nhưng không có vấn đề tương ứng với các giá trị có dấu (vì các giá trị có dấu đã bao gồm các giá trị chưa dấu).
unsigned int x = 0; --x;
và xem những gìx
sẽ trở thành. Nếu không có kiểm tra giới hạn, kích thước có thể đột nhiên nhận được một số giá trị không mong muốn dễ dẫn đến UB.