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


回答:


97

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

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

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


141

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

(違いに不慣れな読者のための背景として、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);

57
とても近い、グレッグ。あなたの説明はほぼ完璧ですが、符号付きの型と負の値の式をシフトすることは実装定義です。ISO / IEC 9899:1999セクション6.5.7を参照してください。
Robᵩ

12
@Rob:実際には、左シフトと符号付き負数の場合、動作は未定義です。
JeremyP 2012

5
実際、左シフトは、結果の数学値(ビットサイズに制限されない)がその符号付きの型で正の値として表現できない場合、正の符号付き値の動作が未定義になります。要するに、符号付きの値を右シフトするときは注意深く踏み込む必要があるということです。
Michael Burr 2013年

3
@supercat:本当にわからない。ただし、動作が未定義のコードが原因でコンパイラーが非常に非直感的な操作を行うケースが文書化されていることは知っています(通常、積極的な最適化が原因です-たとえば、古いLinux TUN / TAPドライバーのnullポインターのバグを参照してください:lwn.net / Articles / 342330)。右シフト(実装で定義された動作であることがわかっている)でのサインフィルが必要でない限り、キャストを使用してそこに到達することを意味する場合でも、通常は符号なしの値を使用してビットシフトを実行しようとします。
Michael Burr

2
@MichaelBurr:ハイパーモダンコンパイラは、C規格で定義されていない動作(実装の 99%で定義されていたとしても)を、すべての動作が完全に定義されているプログラムを有効にする理由として使用していることを知っています実行が期待できるプラットフォームから、有用な動作を行わない価値のない一連の機械命令に。しかし、私は認めます(皮肉なことに)コンパイラの作者が最も大規模な最適化の可能性を逃した理由に戸惑います:到達した場合に関数がネストされる結果となるプログラムの一部を省略します...
supercat

51

TL; DR

検討i及びnシフト演算子の左右のオペランドであること。の型i、整数昇格後のbe 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                |

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


シフティング

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

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

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

数値Xのnによる右算術シフトは、Xが負でない場合にのみ、Xを2 nで整数除算したものと同等です!整数除算は数学的な除算であり、0に向かって丸めます(trunc)。

2の補数エンコーディングで表される負の数の場合、nビットだけ右シフトすると、数学的に2 nで除算され、-∞(floor)に丸められます。したがって、右シフトは非負の値と負の値で異なります。

X≥0の場合、X >> n = X / 2 n = trunc(X÷2 n

X <0の場合、X >> n = floor(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として宣言されshortintシフト演算の結果が暗黙的に変換されます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やVisual C ++などのほとんどの実装では、符号ビットを保持することにより、算術シフトとして右シフトを実装しています。

結論

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


1
素敵な答え。丸めに関して(「シフト」というタイトルのセクション)-右シフト-Infは、負の数と正の数の両方に向かって丸めます。正の数の0への丸めは、への丸めのプライベートケースです-Inf。切り捨てるときは、常に正の重み付けの値をドロップするため、それ以外の場合は正確な結果から減算します。
ysap 2018年

1
@ysapええ、良い観察です。基本的に、正の数の0への丸めは、-∞へのより一般的な丸めの特殊なケースです。これは表で確認できます。ここでは、正と負の両方の数値が-∞に向かって丸くなっていることに気付きました。
legends2k

17

取得するシフトのタイプに関して、重要なことは、シフトする値のタイプです。バグの典型的な原因は、リテラルをシフトして、たとえばビットをマスクしない場合です。たとえば、符号なし整数の左端のビットを削除する場合は、これをマスクとして試すことができます。

~0 >> 1

残念ながら、これにより問題が発生します。シフトされる値(〜0)に符号が付けられ、算術シフトが実行されるため、マスクのすべてのビットが設定されるためです。代わりに、値を符号なしとして明示的に宣言することにより、つまり次のようにして、論理シフトを強制する必要があります。

~0U >> 1;

16

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

7

行う場合-1だけ左シフトすると2が乗算されます-1だけ右シフトすると2で除算されます

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

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

x >> aとx << aでは、条件がa> 0の場合、答えはそれぞれx = x / 2 ^ a、x = x * 2 ^ aです。a<0の場合、答えはどうなりますか?
JAVA

@sunny:必見それはC.で未定義の動作が0よりも小さくなることはない
ジェレミー・

4

まあ、私はそれをウィキペディアで調べました、そして彼らはこれを言っています:

ただし、Cには1つの右シフト演算子>>しかありません。多くのCコンパイラは、シフトする整数のタイプに応じて、実行する右シフトを選択します。多くの場合、符号付き整数は算術シフトを使用してシフトされ、符号なし整数は論理シフトを使用してシフトされます。

だから、それはあなたのコンパイラに依存するように聞こえます。また、その記事では、左シフトは算術と論理で同じであることに注意してください。ボーダーケース(もちろんハイビットセット)でいくつかの符号付きおよび符号なしの数値を使用して簡単なテストを行い、結果がコンパイラでどのようになるかを確認することをお勧めします。Cには標準がないように思えるので、少なくともそのような依存を回避することが妥当かつ可能である場合、どちらか一方に依存することを回避することもお勧めします。


ほとんどのCコンパイラは符号付きの値に対して算術左シフトを使用していましたが、そのような有用な動作は廃止されているようです。現在のコンパイラーの哲学は、変数の左シフトのパフォーマンスにより、コンパイラーは変数が非負でなければならないことを想定し、したがって、変数が負の場合に正しい動作に必要な他のコードを省略できると想定しているようです。
スーパーキャット2015

0

左方移動 <<

これは何とか簡単で、shift演算子を使用する場合は常にビット単位の演算であるため、doubleおよびfloat演算では使用できません。シフト1のゼロを残したときは常に、常に最下位ビット(LSB)にます。

しかし、右シフト>>では、1つの追加ルールに従う必要があり、そのルールは「符号ビットコピー」と呼ばれます。「符号ビットコピー」の意味は、最上位ビット(MSB)が設定されている場合、再度右シフトした後、MSBされている場合リセットされた場合に設定され、再度リセットされます。つまり、前の値がゼロだった場合、再度シフトした後、前のビットが1だった場合、ビットはゼロで、シフト後は再び1になります。このルールは左シフトには適用されません。

負の数を右シフトにシフトした場合の右シフトの最も重要な例。次に、シフト後に値が最終的にゼロに到達し、その後、この-1をシフトした場合、値が何回でも同じままになります。チェックしてください。


0

通常、符号なし変数では論理シフトを使用し、符号付き変数では左シフトを使用します。算術右シフトは、変数を符号拡張するため、本当に重要なシフトです。

他のコンパイラがそうであるように、適切なときにこれを使用します。



-7

多くによると コンパイラ:

  1. << 算術左シフトまたはビット単位左シフトです。
  2. >> 算術右シフトまたはビット単位右シフトです。

3
「算術右シフト」と「ビット単位右シフト」は異なります。それが問題のポイントです。「>>算術かビット単位(論理)か」と質問されました。あなたは「>>算術かビットワイズだ」と答えました。それは質問に答えません。
wchargin 14年

いいえ、<<そして>>演算子は算術ではなく論理的です
shjeff 2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.