C ではシフト演算子 (<<、>>) は算術演算ですか、それとも論理演算ですか?
-
08-06-2019 - |
質問
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);
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 −∞方向に丸めます(床);したがって、右シフトは非負の値と負の値で異なります。
X ≥ 0 の場合、X >> n = X / 2n = 切り捨て(X ÷ 2n)
X < 0 の場合、X >> n = フロア(X ÷ 2)n)
どこ ÷
数学的な割り算です /
整数の除算です。例を見てみましょう:
37)10 = 100101)2
37 ÷ 2 = 18.5
37 / 2 = 18 (18.5 を 0 方向に四捨五入) = 10010)2 [算術右シフトの結果]
-37)10 = 11011011)2 (2の補数、8ビット表現を考慮)
-37 ÷ 2 = -18.5
-37 / 2 = -18 (18.5 を 0 方向に四捨五入) = 11101110)2 [算術右シフトの結果ではありません]
-37 >> 1 = -19 (18.5 を −∞ 方向に四捨五入) = 11101101)2 [算術右シフトの結果]
として ガイ・スティール氏は指摘した, 、この矛盾により、 複数のコンパイラのバグ. 。ここで、非負 (数学) は、符号なしおよび符号付きの非負の値 (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 << E2 の結果は、E1 を左シフトした E2 ビット位置です。空いたビットはゼロで埋められます。E1 が符号なし型の場合、結果の値は E1×2 になります。E2, 、結果の型で表現できる最大値より 1 を加算した剰余を削減します。E1 が符号付き型で負でない値であり、E1×2 の場合E2 結果の型で表現できる場合、それが結果の値になります。それ以外の場合、動作は未定義です。
左シフトは両方とも同じであるため、空いたビットは単純にゼロで埋められます。次に、符号なし型と符号付き型の両方について、それが算術シフトであると述べています。論理シフトはビットで表される値を気にせず、単にビットのストリームとして見るため、私はこれを算術シフトとして解釈しています。しかし、標準ではビットの観点ではなく、E1 と 2 の積によって得られる値の観点から定義されています。E2.
ここでの注意点は、符号付き型の場合、値は負ではなく、結果の値は結果型で表現できる必要があるということです。それ以外の場合、操作は未定義です。 結果の型は、整数昇格を適用した後の E1 の型であり、宛先 (結果を保持する変数) の型ではありません。結果の値は暗黙的に宛先の型に変換されます。その型で表現できない場合、変換は実装定義です (C99 §6.3.1.3/3)。
E1 が負の値を持つ符号付き型の場合、左シフトの動作は未定義です。 これは、見落とされがちな未定義の動作への簡単なルートです。
右シフト
E1 >> E2 の結果は、E1 を右シフトした E2 ビット位置です。E1 が符号なし型である場合、または E1 が符号付き型で負でない値である場合、結果の値は E1/2 の商の整数部分になります。E2. 。E1 の型が符号付きで負の値の場合、結果の値は実装定義です。
符号なしおよび符号付きの非負の値の右シフトは非常に簡単です。空のビットはゼロで埋められます。 符号付き負の値の場合、右シフトの結果は実装定義です。 とはいえ、GCC や ビジュアルC++ 符号ビットを保持することにより、右シフトを算術シフトとして実装します。
結論
特別な演算子を持つ Java とは異なります。 >>>
通常とは異なる論理的なシフトのため >>
そして <<
, C および C++ には算術シフトのみがあり、一部の領域は未定義および実装定義のままになっています。私がこれらを算術演算であると考える理由は、シフトされたオペランドをビットのストリームとして扱うのではなく、演算を数学的に表現する標準的な表現によるものです。おそらくこれが、すべてのケースを論理シフトとして定義するのではなく、これらの領域を未定義または実装定義のままにする理由です。
取得するシフトの種類に関して重要なのは、シフトする値の種類です。古典的なバグの原因は、リテラルをシフトしてビットをマスクオフする場合です。たとえば、符号なし整数の左端のビットを削除したい場合は、これをマスクとして試すことができます。
~0 >> 1
残念ながら、シフトされる値 (~0) は符号付きであり、算術シフトが実行されるため、マスクのすべてのビットが設定されるため、問題が発生します。代わりに、値を符号なしとして明示的に宣言して、論理シフトを強制することをお勧めします。次のようなことを行うことで:
~0U >> 1;
C で int の論理右シフトと算術右シフトを保証する関数を次に示します。
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;
}
あなたがするとき - 左シフト1で左シフトあなたは2-右シフト1を掛けます。
x = 5
x >> 1
x = 2 ( x=5/2)
x = 5
x << 1
x = 10 (x=5*2)
さて、見ました ウィキペディアに載ってるよ, 、彼らはこう言っています。
ただし、Cには1つの正しいシフト演算子のみがあります>>。多くのCコンパイラは、どのタイプの整数がシフトされているかに応じて、どの正しいシフトを実行するかを選択します。多くの場合、署名された整数は算術シフトを使用してシフトされ、符号なしの整数は論理シフトを使用してシフトされます。
したがって、コンパイラに依存しているようです。また、その記事では、左シフトは算術と論理で同じであることに注意してください。境界ケース (もちろん高ビットセット) で符号付きおよび符号なしの数値を使用して簡単なテストを実行し、コンパイラで結果がどのようになるかを確認することをお勧めします。また、C には標準がないようなので、少なくともそのような依存を回避することが合理的で可能であれば、どちらか一方に依存することを避けることをお勧めします。
左方移動 <<
これはなんとなく簡単で、シフト演算子を使用するときは常にビット単位の演算になるため、double 演算や float 演算と一緒に使用することはできません。ゼロを 1 つ左にシフトすると、そのゼロは常に最下位ビットに追加されます (LSB
).
しかし、右シフトでは >>
追加のルールに従う必要があり、そのルールは「サイン ビット コピー」と呼ばれます。「符号ビットコピー」の意味は、最上位ビット(MSB
) が設定され、再度右シフトした後、 MSB
リセットされた場合に再度リセットされると設定されます。つまり、前の値が 0 であった場合、再度シフトした後、ビットは 0 になります。前のビットが 1 であった場合、シフト後は再び 1 になります。この規則は左シフトには適用されません。
右シフトに関する最も重要な例は、負の数値を右シフトにシフトし、その後何度かシフトした後、最終的に値が 0 に達し、その後何度でも -1 シフトしても値は変わりません。チェックしてください。
GCC は行います
for -ve - > 算術シフト
+veの場合 -> 論理シフト
多くの人によると c コンパイラ:
<<
算術左シフトまたはビット単位の左シフトです。>>
は、算術右シフタのビット単位の右シフトです。