Sử dụng số nguyên không dấu trong C và C ++


23

Tôi có một câu hỏi rất đơn giản gây trở ngại cho tôi trong một thời gian dài. Tôi đang xử lý các mạng và cơ sở dữ liệu nên rất nhiều dữ liệu tôi đang xử lý là các bộ đếm 32 bit và 64 bit (không dấu), id nhận dạng 32 bit và 64 bit (cũng không có ánh xạ có ý nghĩa cho dấu hiệu). Tôi thực tế không bao giờ đối phó với bất kỳ vấn đề từ thực sự có thể được thể hiện dưới dạng một số âm.

Tôi và đồng nghiệp của tôi thường xuyên sử dụng các loại không dấu như uint32_tuint64_tcho các vấn đề này và vì nó thường xảy ra nên chúng tôi cũng sử dụng chúng cho các chỉ mục mảng và sử dụng số nguyên phổ biến khác.

Đồng thời, các hướng dẫn mã hóa khác nhau mà tôi đang đọc (ví dụ Google) không khuyến khích sử dụng các kiểu số nguyên không dấu và theo như tôi biết thì cả Java và Scala đều không có kiểu số nguyên không dấu.

Vì vậy, tôi không thể tìm ra đâu là điều đúng đắn: sử dụng các giá trị đã ký trong môi trường của chúng tôi sẽ rất bất tiện, đồng thời hướng dẫn mã hóa để khăng khăng thực hiện chính xác điều này.


Câu trả lời:


31

Có hai trường phái suy nghĩ về điều này, và sẽ không bao giờ đồng ý.

Đầu tiên lập luận rằng có một số khái niệm vốn không được ký - chẳng hạn như các chỉ mục mảng. Không có nghĩa gì khi sử dụng số đã ký cho những số đó vì nó có thể dẫn đến lỗi. Nó cũng có thể áp đặt các giới hạn không cần thiết cho mọi thứ - một mảng sử dụng các chỉ mục 32 bit đã ký chỉ có thể truy cập 2 tỷ mục, trong khi chuyển sang các số 32 bit không dấu cho phép 4 tỷ mục.

Thứ hai lập luận rằng trong bất kỳ chương trình nào sử dụng các số không dấu, sớm hay muộn bạn sẽ thực hiện số học hỗn hợp không dấu. Điều này có thể mang lại kết quả kỳ lạ và bất ngờ: việc tạo một giá trị không dấu lớn cho chữ ký sẽ cho một số âm và ngược lại, việc chuyển một số âm thành không dấu sẽ cho một số dương lớn. Đây có thể là một nguồn lớn của lỗi.


8
Các vấn đề số học hỗn hợp đã ký không dấu được phát hiện bởi trình biên dịch; chỉ cần giữ cho bản dựng của bạn không có cảnh báo (với mức cảnh báo đủ cao). Bên cạnh đó, intngắn hơn để gõ :)
rucamzu

7
Thú nhận: Tôi đang ở trường tư tưởng thứ hai, và mặc dù tôi hiểu các cân nhắc cho các loại không dấu: intlà quá đủ cho các chỉ số mảng 99,99% lần. Các vấn đề số học đã ký - không dấu là phổ biến hơn nhiều, và do đó được ưu tiên về những điều cần tránh. Vâng, trình biên dịch cảnh báo bạn về điều này, nhưng bạn nhận được bao nhiêu cảnh báo khi biên dịch bất kỳ dự án lớn nào? Bỏ qua các cảnh báo là nguy hiểm và thực tiễn xấu, nhưng trong thế giới thực ...
Elias Van Ootegem

11
+1 cho câu trả lời. Thận trọng : Ý kiến ​​thẳng thắn Trước mắt : 1: Phản ứng của tôi đối với trường phái tư tưởng thứ hai là: Tôi đặt cược tiền rằng bất kỳ ai nhận được kết quả bất ngờ từ các loại tích phân không dấu trong C sẽ có hành vi không xác định (và không phải là loại học thuật thuần túy) trong các chương trình C không tầm thường của họ sử dụng các loại tích phân đã ký . Nếu bạn không biết rõ về C để nghĩ rằng các loại không dấu là loại tốt hơn để sử dụng, tôi khuyên bạn nên tránh C. 2: Có chính xác một loại chính xác cho các chỉ mục và kích thước mảng trong C, và đó là size_t, trừ khi có trường hợp đặc biệt lý do tốt khác.
mtraceur

5
Bạn gặp rắc rối mà không có sự ký kết hỗn hợp. Chỉ cần tính unsign int trừ int unsign int.
gnasher729

4
Không quan tâm đến bạn Simon, chỉ với trường phái đầu tiên lập luận rằng "có một số khái niệm vốn không được ký - chẳng hạn như chỉ mục mảng." cụ thể: "Có chính xác một loại chính xác cho các chỉ mục mảng ... trong C," Nhảm nhí! . Chúng tôi DSPers sử dụng các chỉ số tiêu cực tất cả các thời gian. đặc biệt với các đáp ứng xung chẵn hoặc lẻ đối xứng không phải là nguyên nhân. và cho toán LUT. Tôi đang ở trường tư tưởng thứ hai, nhưng tôi nghĩ rằng thật hữu ích khi có cả số nguyên có chữ ký và không dấu trong C và C ++.
robert bristow-johnson

21

Trước hết, hướng dẫn mã hóa Google C ++ không phải là một thứ rất tốt để tuân theo: nó tránh xa những thứ như ngoại lệ, boost, v.v ... vốn là những yếu tố chính của C ++ hiện đại. Thứ hai, chỉ vì một hướng dẫn nhất định hoạt động cho công ty X không có nghĩa là nó sẽ phù hợp với bạn. Tôi sẽ tiếp tục sử dụng các loại không dấu, vì bạn có nhu cầu tốt cho chúng.

Một nguyên tắc nhỏ cho C ++ là: thích inttrừ khi bạn có lý do chính đáng để sử dụng thứ khác.


8
Đó không phải là ý tôi. Các nhà xây dựng là để thiết lập các bất biến, và vì chúng không phải là các hàm nên chúng không thể đơn giản return falsenếu bất biến đó không được thiết lập. Vì vậy, bạn có thể phân tách mọi thứ và sử dụng các hàm init cho các đối tượng của mình hoặc bạn có thể ném std::runtime_error, để ngăn xếp ngăn chặn xảy ra và để tất cả các đối tượng RAII của bạn tự động dọn dẹp và nhà phát triển có thể xử lý ngoại lệ khi thuận tiện cho bạn làm như vậy
bstamour

5
Tôi không thấy cách ứng dụng tạo ra sự khác biệt. Bất cứ khi nào bạn gọi một hàm tạo trên một đối tượng bạn đang thiết lập một bất biến với các tham số. Nếu bất biến đó không thể được đáp ứng, thì bạn cần phải báo hiệu một lỗi khác nếu chương trình của bạn không ở trạng thái tốt. Vì các nhà xây dựng không thể trả lại một cờ, ném một ngoại lệ là một lựa chọn tự nhiên. Vui lòng đưa ra một lập luận chắc chắn về lý do tại sao một ứng dụng kinh doanh sẽ không được hưởng lợi từ phong cách mã hóa như vậy.
bstamour

8
Tôi rất nghi ngờ rằng một nửa số lập trình viên C ++ không có khả năng sử dụng ngoại lệ đúng cách. Nhưng dù sao nếu bạn nghĩ rằng đồng nghiệp của bạn không có khả năng viết C ++ hiện đại thì bằng mọi cách hãy tránh xa C ++ hiện đại.
bstamour

6
@ zzz777 Đừng sử dụng ngoại lệ? Có các nhà xây dựng tư nhân được bao bọc bởi các chức năng nhà máy công cộng nắm bắt các ngoại lệ và làm gì - trả lại một nullptr? trả về một đối tượng "mặc định" (bất cứ điều gì có thể có nghĩa)? Bạn đã không giải quyết bất cứ điều gì - bạn vừa che giấu vấn đề dưới tấm thảm và hy vọng không ai phát hiện ra.
Mael

5
@ zzz777 Nếu bạn định làm hỏng hộp, tại sao bạn quan tâm nếu nó xảy ra từ một ngoại lệ hay signal(6)? Nếu bạn sử dụng một ngoại lệ, 50% các nhà phát triển biết cách đối phó với họ có thể viết mã tốt và phần còn lại có thể được thực hiện bởi các đồng nghiệp của họ.
IllusiveBrian

6

Các câu trả lời khác thiếu ví dụ thực tế, vì vậy tôi sẽ thêm một. Một trong những lý do tại sao tôi (cá nhân) cố gắng tránh các loại không dấu.

Xem xét sử dụng size_t tiêu chuẩn làm chỉ mục mảng:

for (size_t i = 0; i < n; ++i)
    // do something here;

Ok, hoàn toàn bình thường. Sau đó, xem xét chúng tôi quyết định thay đổi hướng của vòng lặp vì một số lý do:

for (size_t i = n - 1; i >= 0; --i)
    // do something here;

Và bây giờ nó không hoạt động. Nếu chúng ta sử dụng intnhư một trình vòng lặp, sẽ không có vấn đề gì. Tôi đã thấy lỗi như vậy hai lần trong hai năm qua. Một khi nó đã xảy ra trong sản xuất và rất khó để gỡ lỗi.

Một lý do khác cho tôi là những cảnh báo khó chịu, khiến bạn viết một cái gì đó như thế này mỗi lần :

int n = 123;  // for some reason n is signed
...
for (size_t i = 0; i < size_t(n); ++i)

Đây là những điều nhỏ, nhưng chúng cộng lại. Tôi cảm thấy như mã sạch hơn nếu chỉ các số nguyên đã ký được sử dụng ở mọi nơi.

Chỉnh sửa: Chắc chắn, các ví dụ trông thật ngu ngốc, nhưng tôi thấy mọi người mắc lỗi này. Nếu có một cách dễ dàng để tránh nó, tại sao không sử dụng nó?

Khi tôi biên dịch đoạn mã sau với VS2015 hoặc GCC, tôi không thấy cảnh báo nào có cài đặt cảnh báo mặc định (ngay cả với -Wall cho GCC). Bạn phải yêu cầu -Wextra để nhận được cảnh báo về điều này trong GCC. Đây là một trong những lý do bạn nên luôn biên dịch với Wall và Wextra (và sử dụng máy phân tích tĩnh), nhưng trong nhiều dự án thực tế, mọi người không làm điều đó.

#include <vector>
#include <iostream>


void unsignedTest()
{
    std::vector<int> v{ 1, 2 };

    for (int i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;

    for (size_t i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;
}

int main()
{
    unsignedTest();
    return 0;
}

Bạn có thể hiểu sai nhiều hơn với các loại đã ký ... Và mã ví dụ của bạn rất hại não và sai lầm rõ ràng, bất kỳ trình biên dịch tử tế nào cũng sẽ cảnh báo nếu bạn yêu cầu cảnh báo.
Ded repeatator

1
Trước đây tôi đã dùng đến những điều kinh khủng như vậy for (size_t i = n - 1; i < n; --i)để làm cho nó hoạt động tốt.
Simon B

2
Nói về vòng lặp for size_tngược lại, có một hướng dẫn mã hóa theo kiểufor (size_t revind = 0u; revind < n; ++revind) { size_t ind = n - 1u - revind; func(ind); }
rwong

2
@rwong Omg, cái này xấu quá. Tại sao không chỉ sử dụng int? :)
Aleksei Petrenko

1
@AlexeyPetrenko - lưu ý rằng cả các tiêu chuẩn C và C ++ hiện tại đều không đảm bảo intđủ lớn để chứa tất cả các giá trị hợp lệ của size_t. Đặc biệt, intcó thể cho phép các số chỉ tối đa 2 ^ 15-1 và thường làm như vậy trên các hệ thống có giới hạn cấp phát bộ nhớ là 2 ^ 16 (hoặc trong một số trường hợp nhất định thậm chí cao hơn). longcó thể là một đặt cược an toàn hơn, mặc dù vẫn không được đảm bảo để làm việc. Chỉ size_tđược đảm bảo để làm việc trên tất cả các nền tảng và trong mọi trường hợp.
Jules

4
for (size_t i = v.size() - 1; i >= 0; --i)
   std::cout << v[i] << std::endl;

Vấn đề ở đây là bạn đã viết vòng lặp một cách thiếu thận trọng dẫn đến hành vi sai lầm. Cấu trúc của vòng lặp giống như người mới bắt đầu được dạy cho các kiểu đã ký (điều này ổn và đúng) nhưng đơn giản là nó không phù hợp với các giá trị không dấu. Nhưng điều này không thể phục vụ như là một đối số chống lại việc sử dụng các loại không dấu, nhiệm vụ ở đây chỉ đơn giản là làm cho vòng lặp của bạn đúng. Và điều này có thể dễ dàng được sửa chữa để làm việc đáng tin cậy cho các loại không dấu như vậy:

for (size_t i = v.size(); i-- > 0; )
    std::cout << v[i] << std::endl;

Sự thay đổi này chỉ đơn giản là hoàn nguyên chuỗi so sánh và thao tác giảm dần và theo tôi là cách hiệu quả nhất, không bị xáo trộn, sạch sẽ và ngắn gọn để xử lý các bộ đếm không dấu trong các vòng lặp ngược. Bạn sẽ làm điều tương tự (bằng trực giác) khi sử dụng vòng lặp while:

size_t i = v.size();
while (i > 0)
{
    --i;
    std::cout << v[i] << std::endl;
}

Không có dòng chảy nào có thể xảy ra, trường hợp của một container rỗng được che đậy hoàn toàn, như trong biến thể nổi tiếng của vòng lặp đã ký và phần thân của vòng lặp có thể không bị thay đổi so với bộ đếm đã ký hoặc vòng lặp phía trước. Bạn chỉ cần làm quen với cấu trúc vòng lặp trông hơi kỳ lạ lúc đầu. Nhưng sau khi bạn thấy rằng hàng tá lần không còn gì khó hiểu nữa.

Tôi sẽ may mắn nếu các khóa học dành cho người mới bắt đầu không chỉ hiển thị vòng lặp chính xác cho chữ ký mà còn cho các loại không dấu. Điều này sẽ tránh được một vài lỗi mà IMHO nên đổ lỗi cho các nhà phát triển không mong muốn thay vì đổ lỗi cho loại không dấu.

HTH


1

Số nguyên không dấu là có lý do.

Ví dụ, xem xét việc bàn giao dữ liệu dưới dạng các byte riêng lẻ, ví dụ như trong gói mạng hoặc bộ đệm tệp. Thỉnh thoảng bạn có thể gặp những con thú như số nguyên 24 bit. Dễ dàng chuyển bit từ ba số nguyên không dấu 8 bit, không dễ dàng với số nguyên có ký hiệu 8 bit.

Hoặc suy nghĩ về các thuật toán sử dụng bảng tra cứu ký tự. Nếu một ký tự là số nguyên không dấu 8 bit, bạn có thể lập chỉ mục bảng tra cứu theo giá trị ký tự. Tuy nhiên, bạn sẽ làm gì nếu ngôn ngữ lập trình không hỗ trợ các số nguyên không dấu? Bạn sẽ có các chỉ mục tiêu cực đến một mảng. Chà, tôi đoán bạn có thể sử dụng một cái gì đó như thế charval + 128nhưng điều đó thật xấu xí.

Thực tế, nhiều định dạng tệp sử dụng số nguyên không dấu và nếu ngôn ngữ lập trình ứng dụng không hỗ trợ số nguyên không dấu, đó có thể là một vấn đề.

Sau đó xem xét số thứ tự TCP. Nếu bạn viết bất kỳ mã xử lý TCP nào, bạn chắc chắn sẽ muốn sử dụng các số nguyên không dấu.

Đôi khi, hiệu quả quan trọng đến mức bạn thực sự cần thêm một chút số nguyên không dấu. Ví dụ, hãy xem xét các thiết bị IoT được vận chuyển hàng triệu. Rất nhiều tài nguyên lập trình sau đó có thể được biện minh để chi cho tối ưu hóa vi mô.

Tôi sẽ lập luận rằng việc biện minh để tránh sử dụng các loại số nguyên không dấu (số học dấu hiệu hỗn hợp, so sánh dấu hiệu hỗn hợp) có thể được khắc phục bằng trình biên dịch với các cảnh báo thích hợp. Các cảnh báo như vậy thường không được bật theo mặc định, nhưng hãy xem ví dụ -Wextrahoặc riêng biệt -Wsign-compare(tự động bật trong C bằng -Wextra, mặc dù tôi không nghĩ rằng nó tự động kích hoạt trong C ++) và -Wsign-conversion.

Tuy nhiên, nếu nghi ngờ, sử dụng một loại đã ký. Nhiều lần, nó là một lựa chọn hoạt động tốt. Và không cho phép những cảnh báo trình biên dịch!


0

Có nhiều trường hợp số nguyên không thực sự đại diện cho số, nhưng ví dụ: mặt nạ bit, id, v.v ... Về cơ bản, trường hợp thêm 1 vào số nguyên không có kết quả có ý nghĩa. Trong những trường hợp đó, sử dụng không dấu.

Có nhiều trường hợp bạn làm số học với số nguyên. Trong những trường hợp này, sử dụng số nguyên có chữ ký, để tránh hành vi sai lệch xung quanh số không. Xem nhiều ví dụ với các vòng lặp, trong đó việc chạy một vòng lặp xuống 0 hoặc sử dụng mã rất không trực quan hoặc bị hỏng do sử dụng các số không dấu. Có lập luận "nhưng các chỉ số không bao giờ âm" - chắc chắn, nhưng sự khác biệt của các chỉ số chẳng hạn là âm.

Trong trường hợp rất hiếm khi các chỉ số vượt quá 2 ^ 31 nhưng không phải 2 ^ 32, bạn không sử dụng số nguyên không dấu, bạn sử dụng số nguyên 64 bit.

Cuối cùng, một cái bẫy đẹp: Trong một vòng lặp "for (i = 0; i <n; ++ i) a [i] ..." nếu tôi không dấu 32 bit và bộ nhớ vượt quá địa chỉ 32 bit, trình biên dịch không thể tối ưu hóa quyền truy cập vào [i] bằng cách tăng một con trỏ, bởi vì tại i = 2 ^ 32 - 1 i kết thúc tốt đẹp. Ngay cả khi n không bao giờ có được lớn như vậy. Sử dụng số nguyên đã ký tránh điều này.


-5

Cuối cùng, tôi đã tìm thấy một câu trả lời thực sự tốt ở đây: "Sổ tay lập trình an toàn" của J.Viega và M.Messier ( http://shop.oreilly.com/product/9780596003944.do )

Các vấn đề bảo mật với số nguyên đã ký:

  1. Nếu chức năng yêu cầu một tham số dương, rất dễ quên việc kiểm tra phạm vi thấp hơn.
  2. Mẫu bit không trực quan từ chuyển đổi kích thước nguyên âm.
  3. Mẫu bit không trực quan được tạo bởi hoạt động dịch chuyển phải của một số nguyên âm.

Có vấn đề với các chuyển đổi không dấu <-> đã ký nên không nên sử dụng kết hợp.


1
Tại sao nó là một câu trả lời tốt? Công thức 3.5 là gì? Nó nói gì về tràn số nguyên vv?
Baldrickk

Theo kinh nghiệm thực tế của tôi Đó là cuốn sách rất hay với những lời khuyên có giá trị về tất cả các khía cạnh khác mà tôi đã thử và nó khá chắc chắn trong khuyến nghị này. So sánh với sự nguy hiểm của số nguyên tràn trên các mảng dài hơn 4G có vẻ khá yếu. Nếu tôi phải xử lý các mảng lớn, chương trình của tôi sẽ có nhiều điều chỉnh tốt để tránh bị phạt hiệu suất.
zzz777

1
Nó không phải là về việc cuốn sách là tốt. Câu trả lời của bạn không cung cấp bất kỳ lời biện minh nào cho việc sử dụng người nhận và không phải ai cũng sẽ có một bản sao của cuốn sách để tra cứu nó. Hãy xem các ví dụ về cách viết một câu trả lời hay
Baldrickk

FYI vừa tìm hiểu về một lý do khác của việc sử dụng các số nguyên không dấu: người ta có thể dễ dàng phát hiện ra quá mức: youtube.com/
Kẻ
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.