シフト演算子 (<<、>>) は C で算術または論理ですか?

左にシフトする場合、算術シフトと論理シフトに違いはありません。右にシフトする場合、シフトのタイプはシフトされる値のタイプによって異なります。

(違いに不慣れな読者のための背景として、1 ビットの「論理的」右シフトは、すべてのビットを右にシフトし、左端のビットを 0 で埋めます。「算術」シフトは、元の値を左端のビットに残します。 . 負の数を扱う場合、違いが重要になります。)

符号なしの値をシフトする場合、C の>> 演算子は論理シフトです。符号付きの値をシフトする場合、>> 演算子は算術シフトです。

たとえば、32 ビット マシンの場合:

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);

K&R 第 2 版によると、符号付き値の右シフトの結果は実装に依存します。

ウィキペディアによると、C/C++ は「通常」、符号付きの値に算術シフトを実装します。

基本的に、コンパイラをテストするか、コンパイラに依存しないようにする必要があります。現在の MS C++ コンパイラに関する私の VS2008 ヘルプには、そのコンパイラが算術シフトを行うと書かれています。


TL;DR

i を検討してください と n それぞれシフト演算子の左オペランドと右オペランドになります。 i の型 、整数昇格後、T . n と仮定すると [0, sizeof(i) * CHAR_BIT) になる — それ以外は未定義 — これらのケースがあります:

| 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                |

† ほとんどのコンパイラはこれを算術シフトとして実装しています
‡ 値が結果タイプ T をオーバーフローする場合は未定義。 i の昇格型

シフティング

1 つ目は、データ型のサイズを気にせずに、数学的な観点から見た論理シフトと算術シフトの違いです。論理シフトは常に破棄されたビットをゼロで埋めますが、算術シフトは左シフトの場合のみゼロで埋めますが、右シフトの場合は MSB をコピーしてオペランドの符号を保持します (負の値の 2 の補数エンコードを想定)。

つまり、論理シフトは、シフトされたオペランドを単なるビット ストリームと見なし、結果の値の符号を気にせずに移動します。算術シフトはそれを (符号付き) 数値と見なし、シフトが行われるときに符号を保持します。

数値 X の n による左算術シフトは、X に 2 n を掛けることと同じです したがって、論理左シフトと同等です。いずれにしても MSB が最後から外れて保存するものがないため、論理シフトでも同じ結果が得られます。

数値 X の n による右算術シフトは、X の 2 n による整数除算と同等です。 X が非負の場合のみ!整数除算は、数学的な除算と 0 (切り捨て) への丸めに他なりません。

2 の補数エンコードで表される負の数の場合、右に n ビットシフトすると、数学的に 2 n で除算されます。 −∞ (床) に向かって丸めます。したがって、右シフトは負でない値と負の値で異なります。

どこで ÷ 算術除算、/ 整数除算です。例を見てみましょう:

Guy Steele が指摘したように、この不一致は複数のコンパイラでバグを引き起こしています。ここで、負でない値 (数学) は、符号なしおよび符号付きの負でない値 (C) にマップできます。どちらも同じように扱われ、右シフトは整数除算によって行われます。

したがって、論理と算術は左シフトでは同等であり、右シフトでは負でない値に対して同等です。それらが異なるのは、負の値の右シフトです。

オペランドと結果の型

標準 C99 §6.5.7 :

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

上記のスニペットでは、両方のオペランドが int になります (整数昇格のため); E2 の場合 負または E2 ≥ sizeof(int) * CHAR_BIT でした その場合、操作は未定義です。これは、使用可能なビットを超えてシフトすると、確実にオーバーフローするためです。 Rだった short として宣言されています 、int シフト演算の結果は暗黙的に short に変換されます;変換先の型で値を表現できない場合、実装定義の動作につながる可能性がある縮小変換。

左シフト

左シフトはどちらも同じなので、空いたビットは単純にゼロで埋められます。次に、符号なしと符号付きの両方の型について、算術シフトであると述べています。論理シフトはビットで表される値を気にしないため、算術シフトとして解釈しています。ビットのストリームとしてそれを見るだけです。しかし、標準はビットの観点からではなく、E1 と 2 E2 の積によって得られる値の観点から定義することによって話します .

ここでの注意点は、符号付きの型の場合、値は非負である必要があり、結果の値は結果の型で表現できる必要があるということです。それ以外の場合、操作は未定義です。 結果の型は、積分昇格を適用した後の E1 の型であり、宛先 (結果を保持する変数) の型ではありません。結果の値は暗黙的に変換先の型に変換されます。その型で表現できない場合、変換は実装定義です (C99 §6.3.1.3/3)。

E1 が負の値を持つ符号付き型の場合、左シフトの動作は未定義です。 これは、見過ごされがちな未定義の動作への簡単なルートです。

右シフト

符号なしおよび符号付きの非負値の右シフトは非常に簡単です。空いているビットはゼロで埋められます。 符号付きの負の値の場合、右シフトの結果は実装定義です。 とはいえ、GCC や Visual C++ などのほとんどの実装では、符号ビットを保持することにより、算術シフトとして右シフトを実装しています。

結論

特別な演算子 >>> を持つ Java とは異なります。 通常の >> とは別の論理シフト用 と << 、C および C++ には算術シフトのみがあり、一部の領域は未定義および実装定義のままです。私がそれらを算術と見なす理由は、シフトされたオペランドをビットのストリームとして扱うのではなく、演算を数学的に表現する標準的な表現によるものです。これがおそらく、すべてのケースを単に論理シフトとして定義するのではなく、これらの領域を未定義/実装定義のままにしておく理由です。