So sánh có dấu / không dấu


85

Tôi đang cố gắng hiểu tại sao đoạn mã sau không đưa ra cảnh báo ở vị trí được chỉ định.

//from limits.h
#define UINT_MAX 0xffffffff /* maximum unsigned int value */
#define INT_MAX  2147483647 /* maximum (signed) int value */
            /* = 0x7fffffff */

int a = INT_MAX;
//_int64 a = INT_MAX; // makes all warnings go away
unsigned int b = UINT_MAX;
bool c = false;

if(a < b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a > b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a <= b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a >= b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a == b) // no warning <--- warning expected here
    c = true;
if(((unsigned int)a) == b) // no warning (as expected)
    c = true;
if(a == ((int)b)) // no warning (as expected)
    c = true;

Tôi nghĩ nó liên quan đến việc quảng bá nền, nhưng hai điều cuối cùng có vẻ nói khác.

Theo suy nghĩ của tôi, ==so sánh đầu tiên chỉ là sự không khớp có dấu / không dấu nhiều như những người khác?


3
gcc 4.4.2 in cảnh báo khi được gọi bằng '
-Wall

Đây là suy đoán nhưng có thể nó tối ưu hóa tất cả các so sánh vì nó biết câu trả lời tại thời điểm biên dịch.
Null Set

2
Ah! lại. nhận xét của bobah: Tôi đã bật tất cả các cảnh báo và cảnh báo bị thiếu hiện xuất hiện. Tôi cho rằng nó đáng lẽ phải xuất hiện ở cài đặt mức cảnh báo giống như các so sánh khác.
Peter

1
@bobah: Tôi thực sự ghét việc gcc 4.4.2 in cảnh báo đó (không có cách nào để bảo nó chỉ in nó vì sự bất bình đẳng), vì mọi cách im lặng cảnh báo đó đều khiến mọi thứ trở nên tồi tệ hơn . Quảng cáo mặc định chuyển đổi đáng tin cậy cả -1 hoặc ~ 0 thành giá trị cao nhất có thể của bất kỳ loại không dấu nào, nhưng nếu bạn tắt tiếng cảnh báo bằng cách tự truyền nó, thì bạn phải biết chính xác loại. Vì vậy, nếu bạn thay đổi loại (mở rộng nó thành dài dài không dấu), các so sánh của bạn với bare -1sẽ vẫn hoạt động (nhưng những điều đó đưa ra cảnh báo) trong khi so sánh của bạn với -1uhoặc (unsigned)-1cả hai sẽ thất bại thảm hại.
Jan Hudec

Tôi không biết tại sao bạn cần một cảnh báo và tại sao các trình biên dịch không thể làm cho nó hoạt động. -1 là số âm nên nhỏ hơn bất kỳ số nào không có dấu. Các mẫu.
CashCow

Câu trả lời:


95

Khi so sánh có dấu với chưa có dấu, trình biên dịch chuyển đổi giá trị có dấu thành không dấu. Đối với sự bình đẳng, điều này không quan trọng -1 == (unsigned) -1,. Để so sánh khác, nó quan trọng, ví dụ sau đây là đúng: -1 > 2U.

CHỈNH SỬA: Tài liệu tham khảo:

5/9: (Biểu thức)

Nhiều toán tử nhị phân mong đợi các toán hạng của kiểu số học hoặc kiểu liệt kê gây ra chuyển đổi và mang lại các kiểu kết quả theo cách tương tự. Mục đích là mang lại một kiểu chung, cũng là kiểu kết quả. Mẫu này được gọi là các chuyển đổi số học thông thường, được định nghĩa như sau:

  • Nếu một trong hai toán hạng thuộc loại kép dài, toán hạng kia sẽ được chuyển thành kép dài.

  • Ngược lại, nếu một trong hai toán hạng là gấp đôi, toán hạng kia sẽ được chuyển thành kép.

  • Ngược lại, nếu một trong hai toán hạng là float, thì toán hạng còn lại sẽ được chuyển thành float.

  • Nếu không, các thăng hạng tích hợp (4.5) sẽ được thực hiện trên cả hai toán hạng.54)

  • Sau đó, nếu một trong hai toán hạng không có dấu dài thì toán hạng kia sẽ được chuyển thành dài không dấu.

  • Ngược lại, nếu một toán hạng là một int dài và một int không dấu khác, thì nếu một int dài có thể đại diện cho tất cả các giá trị của một int không dấu, int unsigned sẽ được chuyển thành một int dài; nếu không thì cả hai toán hạng sẽ được chuyển đổi thành int dài không dấu.

  • Ngược lại, nếu một trong hai toán hạng là dài, toán hạng kia sẽ được chuyển thành dài.

  • Ngược lại, nếu một trong hai toán hạng không có dấu, toán hạng kia sẽ được chuyển thành không dấu.

4,7 / 2: (Chuyển đổi tích phân)

Nếu kiểu đích là không dấu, giá trị kết quả là số nguyên không dấu nhỏ nhất tương ứng với số nguyên nguồn (modulo 2 n trong đó n là số bit được sử dụng để biểu thị kiểu không dấu). [Lưu ý: Trong biểu diễn bổ sung của một hai, chuyển đổi này là khái niệm và không có thay đổi trong mẫu bit (nếu không có sự cắt bớt). ]

EDIT2: Mức cảnh báo MSVC

Tất nhiên, những gì được cảnh báo về các mức cảnh báo khác nhau của MSVC là do các nhà phát triển lựa chọn. Như tôi thấy, lựa chọn của họ liên quan đến bình đẳng có dấu / không dấu so với so sánh lớn hơn / ít hơn có ý nghĩa, điều này tất nhiên là hoàn toàn chủ quan:

-1 == -1có nghĩa giống như -1 == (unsigned) -1- Tôi thấy đó là một kết quả trực quan.

-1 < 2 không có nghĩa giống như -1 < (unsigned) 2- Thoạt nhìn, điều này ít trực quan hơn và IMO xứng đáng được cảnh báo "sớm hơn".


Làm thế nào bạn có thể chuyển đổi đã ký thành chưa ký? Phiên bản không dấu của giá trị có dấu -1 là gì? (có dấu -1 = 1111, trong khi không dấu 15 = 1111, theo bitwise chúng có thể bằng nhau, nhưng chúng không bằng nhau về mặt logic.) Tôi hiểu rằng nếu bạn buộc chuyển đổi này thì nó sẽ hoạt động, nhưng tại sao trình biên dịch lại làm như vậy? Nó phi logic. Hơn nữa, như tôi đã nhận xét ở trên, khi tôi bật các cảnh báo, cảnh báo thiếu dấu == xuất hiện, dường như sao lưu những gì tôi nói?
Peter

1
Như 4.7 / 2 nói, được ký thành không có dấu có nghĩa là không có thay đổi trong mẫu bit cho phần bù của hai. Về lý do tại sao trình biên dịch thực hiện điều này, nó được yêu cầu bởi tiêu chuẩn C ++. Tôi tin rằng lý do đằng sau những cảnh báo của VS ở các mức độ khác nhau là khả năng một biểu thức là không mong muốn - và tôi đồng ý với họ rằng so sánh bình đẳng giữa có dấu / không dấu "ít có khả năng" là một vấn đề hơn so với so sánh bất bình đẳng. Tất nhiên, điều này là chủ quan - đây là những lựa chọn của các nhà phát triển trình biên dịch VC.
Erik

Ok, tôi nghĩ rằng tôi gần như hiểu được. Cách tôi đọc, trình biên dịch là (về mặt khái niệm) đang làm: 'if (((unsigned _int64) 0x7fffffff) == ((unsigned _int64) 0xffffffff))', bởi vì _int64 là kiểu nhỏ nhất có thể đại diện cho cả 0x7fffffff và 0xffffffff trong điều khoản không dấu?
Peter

2
Trên thực tế so sánh với (unsigned)-1hoặc -1uthường kém hơn so với so sánh với -1. Đó là bởi vì (unsigned __int64)-1 == -1, nhưng (unsigned __int64)-1 != (unsigned)-1. Vì vậy, nếu trình biên dịch đưa ra cảnh báo, bạn cố gắng tắt nó bằng cách truyền sang không dấu hoặc sử dụng -1uvà nếu giá trị thực sự là 64-bit hoặc bạn tình cờ thay đổi nó thành một sau đó, bạn sẽ phá vỡ mã của mình! Và hãy nhớ rằng đó size_tlà không có dấu, 64-bit chỉ trên nền tảng 64-bit và sử dụng -1 cho giá trị không hợp lệ là rất phổ biến với nó.
Jan Hudec

1
Có lẽ các cpmpilers không nên làm điều đó sau đó. Nếu nó so sánh có dấu và chưa ký, chỉ cần kiểm tra xem giá trị đã ký có âm hay không. Nếu vậy nó được đảm bảo là nhỏ hơn không có dấu bất kể.
CashCow

32

Tại sao các cảnh báo có chữ ký / không dấu lại quan trọng và các lập trình viên phải chú ý đến chúng, được minh họa bằng ví dụ sau.

Đoán đầu ra của mã này?

#include <iostream>

int main() {
        int i = -1;
        unsigned int j = 1;
        if ( i < j ) 
            std::cout << " i is less than j";
        else
            std::cout << " i is greater than j";

        return 0;
}

Đầu ra:

i is greater than j

Ngạc nhiên? Demo trực tuyến: http://www.ideone.com/5iCxY

Tóm tắt: so sánh, nếu một toán hạng là unsigned, thì toán hạng khác được chuyển đổi ngầm thành unsigned nếu kiểu của nó là có dấu!


2
Anh ấy đúng! Thật ngu ngốc, nhưng anh ấy nói đúng. Đây là một gotcha lớn mà tôi chưa từng xem qua. Tại sao nó không chuyển đổi giá trị chưa ký thành giá trị có dấu (lớn hơn) ?! Nếu bạn làm "if (i <((int) j))" thì nó hoạt động như bạn mong đợi. Mặc dù "if (i <((_int64) j))" sẽ có ý nghĩa hơn (giả sử bạn không thể, _int64 đó lớn gấp đôi kích thước của int).
Peter

6
@Peter "Tại sao nó không chuyển đổi giá trị không hợp vệ sinh thành giá trị có dấu (lớn hơn)?" Câu trả lời rất đơn giản: có thể không có giá trị có dấu lớn hơn. Trên máy 32 bit, trong những ngày trước đó, cả int và long đều là 32 bit, và không có gì lớn hơn. Khi so sánh có dấu và chưa có dấu, các trình biên dịch C ++ sớm nhất đã chuyển đổi cả hai thành có dấu. Vì tôi quên lý do gì, ủy ban tiêu chuẩn C đã thay đổi điều này. Giải pháp tốt nhất của bạn là tránh không có dấu càng nhiều càng tốt.
James Kanze,

5
@JamesKanze: Tôi nghi ngờ nó cũng phải làm gì đó với thực tế, rằng kết quả của tràn có dấu là Hành vi không xác định trong khi kết quả của tràn chưa dấu thì không và do đó việc chuyển đổi giá trị có dấu âm thành không dấu được xác định trong khi chuyển đổi giá trị lớn chưa dấu thành dấu âm giá trị không .
Jan Hudec

2
@James Trình biên dịch luôn có thể tạo hợp ngữ để triển khai ngữ nghĩa trực quan hơn của phép so sánh này mà không cần truyền sang một số kiểu lớn hơn. Trong ví dụ cụ thể này, trước tiên chỉ cần kiểm tra xem i<0. Sau đó ilà nhỏ hơn jchắc chắn. Nếu ikhông nhỏ hơn 0, thì ìcó thể được chuyển đổi một cách an toàn thành không dấu để so sánh với nó j. Chắc chắn, so sánh giữa có ký và chưa ký sẽ chậm hơn, nhưng kết quả của chúng sẽ đúng hơn theo một nghĩa nào đó.
Sven

@Sven tôi đồng ý. Tiêu chuẩn có thể yêu cầu các phép so sánh hoạt động cho tất cả các giá trị thực tế, thay vì chuyển đổi thành một trong hai loại. Tuy nhiên, điều này sẽ chỉ hoạt động để so sánh; Tôi nghi ngờ rằng ủy ban không muốn có các quy tắc khác nhau cho phép so sánh và các thao tác khác (và không muốn tấn công vấn đề chỉ định phép so sánh khi kiểu thực sự được so sánh không tồn tại).
James Kanze

4

Toán tử == chỉ thực hiện một phép so sánh theo từng bit (bằng phép chia đơn giản để xem nó có phải là 0 hay không).

So sánh nhỏ hơn / lớn hơn phụ thuộc nhiều hơn vào dấu hiệu của số.

4 bit Ví dụ:

1111 = 15? hoặc -1?

vì vậy nếu bạn có 1111 <0001 ... thì rất mơ hồ ...

nhưng nếu bạn có 1111 == 1111 ... Điều tương tự mặc dù bạn không cố ý.


Tôi hiểu điều này, nhưng nó không trả lời câu hỏi của tôi. Như bạn đã chỉ ra, 1111! = 1111 nếu các dấu hiệu không khớp. Trình biên dịch biết có sự không phù hợp từ các loại, vậy tại sao nó không cảnh báo về điều đó? (Quan điểm của tôi là mã của tôi có thể chứa nhiều điểm không khớp như vậy mà tôi không được cảnh báo.)
Peter

Đó là cách nó được thiết kế. Phép thử bình đẳng kiểm tra tính tương tự. Và nó cũng tương tự. Tôi đồng ý với bạn rằng nó không nên theo cách này. Bạn có thể làm một macro hoặc cái gì đó quá tải x == y là ((x <y) || (x> y))!
Yochai Timmer

1

Trong một hệ thống đại diện cho các giá trị bằng cách sử dụng 2 bổ sung (bộ xử lý hiện đại nhất), chúng bằng nhau ngay cả ở dạng nhị phân. Đây có thể là lý do tại sao trình biên dịch không phàn nàn về a == b .

Và đối với tôi, trình biên dịch kỳ lạ không cảnh báo bạn về dấu == ((int) b) . Tôi nghĩ rằng nó sẽ cung cấp cho bạn một cảnh báo cắt bớt số nguyên hoặc một cái gì đó.


1
Triết lý của C / C ++ là: trình biên dịch tin tưởng rằng nhà phát triển biết (các) anh ta đang làm gì khi chuyển đổi rõ ràng giữa các kiểu. Do đó, không có cảnh báo (ít nhất là theo mặc định - tôi tin rằng có những trình biên dịch tạo ra cảnh báo cho việc này nếu mức cảnh báo được đặt cao hơn mặc định).
Péter Török

0

Dòng mã được đề cập không tạo ra cảnh báo C4018 vì Microsoft đã sử dụng một số cảnh báo khác (tức là C4389 ) để xử lý trường hợp đó và C4389 không được bật theo mặc định (tức là ở cấp 3).

Từ tài liệu của Microsoft cho C4389:

// C4389.cpp
// compile with: /W4
#pragma warning(default: 4389)

int main()
{
   int a = 9;
   unsigned int b = 10;
   if (a == b)   // C4389
      return 0;
   else
      return 0;
};

Các câu trả lời khác đã giải thích khá rõ tại sao Microsoft có thể quyết định tạo một trường hợp đặc biệt từ toán tử bình đẳng, nhưng tôi thấy những câu trả lời đó không thực sự hữu ích nếu không đề cập đến C4389 hoặc cách kích hoạt nó trong Visual Studio .

Tôi cũng nên đề cập rằng nếu bạn định bật C4389, bạn cũng có thể xem xét bật C4388. Thật không may là không có tài liệu chính thức cho C4388 nhưng nó dường như bật lên trong các biểu thức như sau:

int a = 9;
unsigned int b = 10;
bool equal = (a == b); // C4388
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.