Các toán tử thay đổi (<<, >>) là số học hay logic trong C?


Câu trả lời:


97

Theo phiên bản 2 của K & R , kết quả phụ thuộc vào việc thực hiện các thay đổi bên phải của các giá trị đã ký.

Wikipedia nói rằng C / C ++ 'thường' thực hiện một sự thay đổi số học trên các giá trị đã ký.

Về cơ bản, bạn cần kiểm tra trình biên dịch của mình hoặc không dựa vào nó. Trợ giúp VS2008 của tôi cho trình biên dịch MS C ++ hiện tại nói rằng trình biên dịch của họ thực hiện dịch chuyển số học.


141

Khi dịch chuyển sang trái, không có sự khác biệt giữa dịch chuyển số học và logic. Khi dịch chuyển sang phải, loại dịch chuyển phụ thuộc vào loại giá trị được dịch chuyển.

. . Sự khác biệt trở nên quan trọng khi xử lý các số âm.)

Khi dịch chuyển một giá trị không dấu, toán tử >> trong C là một sự thay đổi logic. Khi dịch chuyển một giá trị đã ký, toán tử >> là một sự thay đổi số học.

Ví dụ: giả sử máy 32 bit:

signed int x1 = 5;
assert((x1 >> 1) == 2);
signed int x2 = -5;
assert((x2 >> 1) == -3);
unsigned int x3 = (unsigned int)-5;
assert((x3 >> 1) == 0x7FFFFFFD);

57
Rất gần, Greg. Giải thích của bạn gần như hoàn hảo, nhưng việc thay đổi biểu thức của loại đã ký và giá trị âm được xác định theo thực thi. Xem ISO / IEC 9899: 1999 Mục 6.5.7.
Cướp

12
@Rob: Trên thực tế, đối với ca trái và số âm đã ký, hành vi không được xác định.
JeremyP

5
Trên thực tế, dịch chuyển trái cũng dẫn đến hành vi không xác định cho các giá trị được ký dương nếu giá trị toán học kết quả (không bị giới hạn ở kích thước bit) không thể được biểu thị dưới dạng giá trị dương trong loại đã ký. Điểm mấu chốt là bạn phải bước cẩn thận khi dịch chuyển đúng một giá trị đã ký.
Michael Burr

3
@supercat: Tôi thực sự không biết. Tuy nhiên, tôi biết rằng có những trường hợp được ghi lại trong đó mã có hành vi không xác định khiến trình biên dịch thực hiện những việc không trực quan (thường là do tối ưu hóa mạnh mẽ - ví dụ: xem lỗi con trỏ null trình điều khiển Linux TUN / TAP cũ: lwn.net / Bài viết / 342330 ). Trừ khi tôi cần điền vào ký hiệu thay đổi bên phải (mà tôi nhận thấy là hành vi được xác định thực hiện), tôi thường cố gắng thực hiện các thay đổi bit của mình bằng cách sử dụng các giá trị không dấu, ngay cả khi điều đó có nghĩa là sử dụng phôi để đến đó.
Michael Burr

2
@MichaelBurr: Tôi biết rằng trình biên dịch hypermodern sử dụng thực tế là hành vi không được xác định bởi tiêu chuẩn C (mặc dù nó đã được xác định trong 99% triển khai ) như là một biện minh để biến các chương trình có hành vi được xác định đầy đủ trên tất cả các nền tảng nơi chúng có thể được mong đợi chạy, thành một loạt các hướng dẫn máy vô giá trị mà không có hành vi hữu ích. Mặc dù vậy, tôi sẽ thừa nhận (tôi mỉa mai) Tôi bối rối tại sao các tác giả trình biên dịch đã bỏ lỡ khả năng tối ưu hóa lớn nhất: bỏ qua bất kỳ phần nào của chương trình, nếu đạt được, sẽ dẫn đến các hàm được lồng vào nhau ...
supercat

51

TL; DR

Xem xét inlà các toán hạng trái và phải tương ứng của một toán tử thay đổi; loại i, sau khuyến mãi số nguyên, được T. Giả sử nlà trong [0, sizeof(i) * CHAR_BIT)- không xác định khác - chúng tôi có những trường hợp sau:

| Direction  |   Type   | Value (i) | Result                   |
| ---------- | -------- | --------- | ------------------------ |
| Right (>>) | unsigned |     0    | −∞  (i ÷ 2ⁿ)            |
| Right      | signed   |     0    | −∞  (i ÷ 2ⁿ)            |
| Right      | signed   |    < 0    | Implementation-defined  |
| Left  (<<) | unsigned |     0    | (i * 2ⁿ) % (T_MAX + 1)   |
| Left       | signed   |     0    | (i * 2ⁿ)                |
| Left       | signed   |    < 0    | Undefined                |

Hầu hết các trình biên dịch thực hiện điều này dưới dạng dịch chuyển số học
không xác định nếu giá trị vượt quá loại kết quả T; loại quảng cáo của tôi


Dịch chuyển

Đầu tiên là sự khác biệt giữa các thay đổi logic và số học từ quan điểm toán học, mà không phải lo lắng về kích thước loại dữ liệu. Các dịch chuyển logic luôn lấp đầy các bit bị loại bỏ bằng 0 trong khi dịch chuyển số học chỉ lấp đầy các số 0 cho dịch chuyển trái, nhưng đối với dịch chuyển phải, nó sao chép MSB do đó giữ nguyên dấu của toán hạng (giả sử mã hóa bổ sung hai cho các giá trị âm).

Nói cách khác, sự dịch chuyển logic xem toán hạng đã dịch chuyển như một luồng bit và di chuyển chúng, mà không bận tâm đến dấu hiệu của giá trị kết quả. Sự thay đổi số học xem nó như một số (đã ký) và giữ nguyên dấu hiệu khi các ca được thực hiện.

Sự dịch chuyển số học bên trái của một số X với n tương đương với nhân X với 2 n và do đó tương đương với dịch chuyển trái logic; một sự thay đổi hợp lý cũng sẽ cho kết quả tương tự vì dù sao MSB cũng rơi vào tình trạng cuối cùng và không có gì để bảo tồn.

Một sự dịch chuyển số học đúng của một số X theo n tương đương với phép chia số nguyên của X cho 2 n CHỈ nếu X không âm! Phân chia số nguyên không có gì ngoài phân chia toán học và làm tròn về 0 ( trunc ).

Đối với các số âm, được biểu thị bằng mã hóa bổ sung của hai, dịch chuyển sang phải bởi n bit có tác dụng chia toán học cho 2 n và làm tròn về phía ( sàn ); do đó dịch chuyển phải là khác nhau cho các giá trị không âm và âm.

cho X ≥ 0, X >> n = X / 2 n = trunc (X ÷ 2 n )

cho X <0, X >> n = sàn (X 2 n )

trong đó ÷phân chia toán học, /là phân chia số nguyên. Hãy xem xét một ví dụ:

37) 10 = 100101) 2

37 2 = 18,5

37/2 = 18 (làm tròn 18,5 về 0) = 10010) 2 [kết quả của sự dịch chuyển số học bên phải]

-37) 10 = 11011011) 2 (xem xét phần bù hai, biểu diễn 8 bit)

-37 2 = -18,5

-37 / 2 = -18 (làm tròn 18,5 về 0) = 11101110) 2 [KHÔNG phải là kết quả của sự dịch chuyển số học bên phải]

-37 >> 1 = -19 (làm tròn 18,5 về phía) = 11101101) 2 [kết quả của sự dịch chuyển số học bên phải]

Như Guy Steele đã chỉ ra , sự khác biệt này đã dẫn đến các lỗi trong nhiều trình biên dịch . Ở đây không âm (toán học) có thể được ánh xạ tới các giá trị không âm và không dấu (C); cả hai đều được xử lý như nhau và dịch chuyển sang phải được thực hiện bằng cách chia số nguyên.

Vì vậy, logic và số học là tương đương trong dịch chuyển trái và cho các giá trị không âm trong dịch chuyển phải; đó là sự dịch chuyển đúng của các giá trị âm mà chúng khác nhau.

Các loại toán hạng và kết quả

Tiêu chuẩn C99 §6.5.7 :

Mỗi toán hạng sẽ có các kiểu số nguyên.

Các chương trình khuyến mãi số nguyên được thực hiện trên mỗi toán hạng. Loại kết quả là toán hạng bên trái được thăng cấp. Nếu giá trị của toán hạng bên phải là âm hoặc lớn hơn hoặc bằng chiều rộng của toán hạng bên trái được thăng cấp, hành vi không được xác định.

short E1 = 1, E2 = 3;
int R = E1 << E2;

Trong đoạn mã trên, cả hai toán hạng trở thành int(do quảng cáo số nguyên); nếu E2là âm hoặc E2 ≥ sizeof(int) * CHAR_BITsau đó hoạt động là không xác định. Điều này là do dịch chuyển nhiều hơn các bit có sẵn chắc chắn sẽ tràn. Đã Rđược tuyên bố là short, intkết quả của hoạt động thay đổi sẽ được chuyển đổi hoàn toàn thành short; một chuyển đổi thu hẹp, có thể dẫn đến hành vi được xác định thực hiện nếu giá trị không thể biểu thị trong loại đích.

Dịch trái

Kết quả của E1 << E2 là vị trí bit E2 dịch chuyển trái; bit trống được điền với số không. Nếu E1 có loại không dấu, giá trị của kết quả là E1 × 2 E2 , giảm modulo một lần so với giá trị tối đa có thể biểu thị trong loại kết quả. Nếu E1 có loại đã ký và giá trị không âm và E1 × 2 E2 có thể biểu thị trong loại kết quả, thì đó là giá trị kết quả; mặt khác, hành vi là không xác định.

Vì các dịch chuyển trái là giống nhau cho cả hai, các bit bị bỏ trống chỉ đơn giản là chứa các số không. Sau đó, tuyên bố rằng đối với cả hai loại không dấu và có chữ ký, đó là một sự thay đổi số học. Tôi đang giải thích nó như là sự dịch chuyển số học vì các dịch chuyển logic không bận tâm về giá trị được biểu thị bởi các bit, nó chỉ xem nó như một luồng bit; nhưng tiêu chuẩn nói không phải về các bit, mà bằng cách định nghĩa nó theo giá trị thu được từ sản phẩm của E1 với 2 E2 .

Thông báo trước ở đây là đối với các loại đã ký, giá trị phải không âm và giá trị kết quả phải được biểu thị trong loại kết quả. Nếu không, hoạt động là không xác định. Loại kết quả sẽ là loại của E1 sau khi áp dụng khuyến mãi tích phân và không phải là đích (biến sẽ giữ kết quả). Giá trị kết quả được chuyển đổi hoàn toàn sang loại đích; nếu nó không thể biểu diễn trong loại đó, thì chuyển đổi được xác định theo triển khai (C99 §6.3.1.3 / 3).

Nếu E1 là loại đã ký có giá trị âm thì hành vi dịch chuyển trái không được xác định. Đây là một lộ trình dễ dàng đến hành vi không xác định có thể dễ dàng bị bỏ qua.

Phải dịch chuyển

Kết quả của E1 >> E2 là vị trí bit E2 dịch chuyển sang phải. Nếu E1 có loại không dấu hoặc nếu E1 có loại đã ký và giá trị không âm, giá trị của kết quả là phần không thể thiếu của thương số của E1 / 2 E2 . Nếu E1 có loại đã ký và giá trị âm, giá trị kết quả được xác định theo thực hiện.

Sự thay đổi bên phải đối với các giá trị không âm và không dấu được ký là khá đơn giản; các bit còn trống chứa đầy số không. Đối với các giá trị âm đã ký, kết quả của dịch chuyển phải được xác định theo thực hiện. Điều đó nói rằng, hầu hết các triển khai như GCC và Visual C ++ đều thực hiện dịch chuyển sang phải dưới dạng dịch chuyển số học bằng cách bảo toàn bit dấu.

Phần kết luận

Không giống như Java, có một toán tử đặc biệt >>>để dịch chuyển logic ngoài thông thường >><<, C và C ++ chỉ có dịch chuyển số học với một số khu vực không được xác định và xác định thực hiện. Lý do tôi coi chúng là số học là do cách diễn đạt chuẩn của phép toán hơn là coi toán hạng dịch chuyển như một luồng bit; đây có lẽ là lý do tại sao nó khiến các khu vực đó không được xác định / triển khai thay vì chỉ xác định tất cả các trường hợp là thay đổi logic.


1
Câu trả lời tốt đẹp. Liên quan đến làm tròn số (trong phần có tiêu đề Dịch chuyển ) - làm tròn dịch chuyển sang phải -Infcho cả số âm và số dương. Làm tròn về 0 của một số dương là một trường hợp riêng của làm tròn theo hướng -Inf. Khi cắt ngắn, bạn luôn giảm các giá trị có trọng số dương, do đó bạn trừ đi kết quả chính xác khác.
ysap

1
@ysap Vâng, quan sát tốt. Về cơ bản, vòng về 0 đối với các số dương là trường hợp đặc biệt của vòng tổng quát hơn về phía −∞; điều này có thể được nhìn thấy trong bảng, trong đó cả số dương và số âm tôi đã lưu ý nó là tròn về phía.
huyền thoại2k

17

Về loại dịch chuyển bạn nhận được, điều quan trọng là loại giá trị mà bạn đang thay đổi. Một nguồn lỗi cổ điển là khi bạn chuyển một nghĩa đen sang, nói, che dấu các bit. Ví dụ: nếu bạn muốn bỏ phần lớn nhất bên trái của một số nguyên không dấu, thì bạn có thể thử điều này làm mặt nạ của mình:

~0 >> 1

Thật không may, điều này sẽ khiến bạn gặp rắc rối vì mặt nạ sẽ có tất cả các bit của nó được đặt vì giá trị được dịch chuyển (~ 0) được ký, do đó, một sự thay đổi số học được thực hiện. Thay vào đó, bạn muốn buộc một sự thay đổi hợp lý bằng cách tuyên bố rõ ràng giá trị là không dấu, tức là bằng cách làm như thế này:

~0U >> 1;

16

Dưới đây là các hàm để đảm bảo dịch chuyển phải logic và dịch chuyển số học phải của một số nguyên trong C:

int logicalRightShift(int x, int n) {
    return (unsigned)x >> n;
}
int arithmeticRightShift(int x, int n) {
    if (x < 0 && n > 0)
        return x >> n | ~(~0U >> n);
    else
        return x >> n;
}

7

Khi bạn làm - dịch chuyển trái sang 1, bạn nhân với 2 - dịch chuyển phải cho 1 bạn chia cho 2

 x = 5
 x >> 1
 x = 2 ( x=5/2)

 x = 5
 x << 1
 x = 10 (x=5*2)

Trong x >> a và x << a nếu điều kiện là a> 0 thì câu trả lời lần lượt là x = x / 2 ^ a, x = x * 2 ^ a thì câu trả lời sẽ là gì nếu a <0?
JAVA

@sunny: a không được nhỏ hơn 0. Đó là hành vi không xác định trong C.
Jeremy

4

Chà, tôi đã tra cứu nó trên wikipedia và họ có điều này để nói:

C, tuy nhiên, chỉ có một toán tử dịch chuyển bên phải, >>. Nhiều trình biên dịch C chọn dịch chuyển đúng nào để thực hiện tùy thuộc vào loại số nguyên nào đang được dịch chuyển; các số nguyên được ký thường được dịch chuyển bằng cách sử dụng dịch chuyển số học và các số nguyên không dấu được dịch chuyển bằng cách sử dụng dịch chuyển logic.

Vì vậy, có vẻ như nó phụ thuộc vào trình biên dịch của bạn. Cũng trong bài viết đó, lưu ý rằng dịch chuyển trái là giống nhau cho số học và logic. Tôi sẽ khuyên bạn nên thực hiện một thử nghiệm đơn giản với một số số đã ký và không dấu trên trường hợp viền (tất nhiên là tập bit cao) và xem kết quả trên trình biên dịch của bạn là gì. Tôi cũng khuyên bạn nên tránh phụ thuộc vào nó là cái này hay cái kia vì dường như C không có tiêu chuẩn, ít nhất là nếu nó hợp lý và có thể để tránh sự phụ thuộc như vậy.


Mặc dù hầu hết các trình biên dịch C được sử dụng để có dịch chuyển trái số học cho các giá trị đã ký, nhưng hành vi hữu ích như vậy dường như đã bị phản đối. Triết lý trình biên dịch hiện tại dường như giả định rằng hiệu suất của dịch chuyển trái trên một biến cho phép trình biên dịch giả định rằng biến đó không âm và do đó bỏ qua bất kỳ mã nào ở nơi khác cần thiết cho hành vi đúng nếu biến đó là âm .
supercat

0

Dịch trái <<

Điều này bằng cách nào đó dễ dàng và bất cứ khi nào bạn sử dụng toán tử thay đổi, nó luôn luôn là một hoạt động khôn ngoan, vì vậy chúng tôi không thể sử dụng nó với một hoạt động kép và nổi. Bất cứ khi nào chúng tôi rời ca một số 0, nó luôn được thêm vào bit có ý nghĩa nhỏ nhất ( LSB).

Nhưng trong ca làm việc đúng, >>chúng ta phải tuân theo một quy tắc bổ sung và quy tắc đó được gọi là "sao chép bit dấu". Ý nghĩa của "sao chép bit dấu" là nếu bit quan trọng nhất ( MSB) được đặt thì sau khi dịch chuyển phải một lần nữa, MSBnó sẽ được đặt nếu được đặt lại sau đó được đặt lại, nghĩa là nếu giá trị trước đó bằng 0 thì sau khi dịch lại lần nữa, bit bằng 0 nếu bit trước đó là một thì sau khi dịch chuyển nó lại là một. Quy tắc này không áp dụng cho một ca trái.

Ví dụ quan trọng nhất về dịch chuyển phải nếu bạn dịch chuyển bất kỳ số âm nào sang dịch chuyển sang phải, sau đó sau một số thay đổi, giá trị cuối cùng sẽ về 0 và sau đó nếu thay đổi này -1 bất kỳ số lần nào giá trị sẽ giữ nguyên. Hãy kiểm tra.


0

thông thường sẽ sử dụng các thay đổi logic trên các biến không dấu và cho các dịch chuyển trái trên các biến đã ký. Sự thay đổi quyền số học là một trong những thực sự quan trọng bởi vì nó sẽ ký mở rộng biến.

sẽ sử dụng điều này khi áp dụng, như các trình biên dịch khác có khả năng làm.


-1

GCC nào

  1. cho -ve -> Dịch chuyển số học

  2. Dành cho + ve -> Dịch chuyển logic


-7

Theo nhiều người trình biên dịch:

  1. << là một sự thay đổi số học hoặc thay đổi trái bit bit.
  2. >> là một sự thay đổi bên phải số học bit shift phải.

3
"Dịch chuyển số học đúng" và "dịch chuyển bit phải" khác nhau. Đó là điểm của câu hỏi. Câu hỏi được hỏi, "Là >>số học hay bitwise (logic)?" Bạn đã trả lời " >>là số học hoặc bitwise." Điều đó không trả lời câu hỏi.
wchargein

Không, <<và các >>toán tử là logic, không phải là số học
shjeff
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.