C言語基礎|論理シフトと算術シフト

同じ右シフトでも結果が変わる!?論理シフトと算術シフトで“負数のビットの動き”をスッキリ整理しよう。

論理シフトと算術シフトは「負数の右シフト」を理解する鍵

シフト演算(<< や >>)は、ビット列を左や右にずらす操作でしたね。
でも、負の整数を右にずらすときは話が少しややこしくなります。

理由はカンタンで、負数は最上位ビット(符号ビット)が 1 になっていることが多く、右にずらしたときに

  • 左側の空いたビットを 0 で埋めるのか
  • それとも 1(符号ビット)で埋めるのか

で、結果がまったく変わってしまうからです。

この「埋め方」の代表が、論理シフト算術シフトです。
C言語では、符号付き負数の右シフトは処理系依存になりやすいので、ここをちゃんと理解しておくと安全なコードが書けるようになります。

まずは用語の整理:論理シフトと算術シフト

提示文の内容を、学習用に表でまとめます。

論理シフトと算術シフトの違い(右シフトのイメージ)

名前何を基準にシフトする?空いた上位ビットの埋め方負数を右シフトした結果
論理シフト(logical shift)符号ビットも含めて全部をそのまま0 で埋める0 または正の値になりやすい
算術シフト(arithmetic shift)符号を保つことを優先元の符号ビット(0 or 1)で埋める符号が変わりにくい(負のまま)

この表の説明

  • 論理シフトは「ビット列としての右シフト」です。符号なんて知らないよ、という感じ。
  • 算術シフトは「符号付き整数としての右シフト」です。符号を維持しやすい動きになります。
  • どちらが使われるかは処理系や型(signed/unsigned)で変わるので、C言語では注意が必要です。

負の値を右に1ビットずらしたときの違い

図:負の整数の右シフト(論理シフト vs 算術シフト)

元(負の値の例): 1 xxx xxxx xxxx xxxx   (先頭の1が符号ビット)

a) 論理シフト(右へ1)
   0 1xx xxxx xxxx xxxx   ← 左端は0で埋める
   → 符号ビットが0になるので、0または正になりやすい

b) 算術シフト(右へ1)
   1 1xx xxxx xxxx xxxx   ← 左端は元の符号ビット(1)で埋める
   → 負のままになりやすい

この図の説明

  • “右シフト”の共通部分は「全体が右へずれる」ことです。
  • 違いは、左端(上位側)の空きを何で埋めるかだけ。
  • ところがその 1ビットが、符号(正負)を左右するので結果が激変します。

算術シフトは「だいたい 1/2」っぽく見える理由

算術シフトは、符号ビットを維持する方向に動くので、負数でも「割り算っぽい」動きに見えます。

表:算術右シフトが割り算っぽい理由(イメージ)

操作直感注意
x >> 1(算術シフト)値がおおむね 1/2 になる端数の丸めが絡む(負数は特に)
x << 1値がおおむね 2倍になるオーバーフローに注意

この表の説明
2進数は 1ビット動くと重みが変わるので、シフトが倍率変化に見えます。
ただし負数のときは、丸め方向(切り捨て/切り上げ相当)の差で、単純に割り算と一致しない場面が出ます。

C言語での超重要ポイント:unsigned なら論理シフトの動きになりやすい

C言語では、一般的に

  • unsigned の右シフトは、上位が 0 で埋まる(論理シフトのイメージ)
  • signed の右シフト(特に負数)は処理系依存になりやすい

と考えると安全です。

安全な方針

やりたいことおすすめ理由
ビット列として扱いたいunsigned を使う符号のややこしさを避けられる
負数を右にずらしたいできるだけ避ける結果が処理系依存になりやすい
どうしても signed の挙動確認が必要実験して確認する環境差が出るから

この表の説明
教材としては、まず unsigned 中心でビット操作に慣れるのが一番スムーズです。
「負数のシフトは罠がある」ことだけ先に知っておけばOKです。

登場する命令(演算子)の書式と「何をする命令か」

ここで扱う命令(演算子)はシンプルです。

今回の主役(シフト演算子)

演算子書式何をする命令?
>>a >> ba のビット列を b ビット右にずらした値を作る
<<a << ba のビット列を b ビット左にずらした値を作る

代入を伴う形(よく使う)

演算子書式意味
>>=a >>= ba を右に b ビットずらして a に代入する
<<=a <<= ba を左に b ビットずらして a に代入する

この表の説明

  • や << は「新しい値を生成」
  • = や <<= は「ずらした結果で自分を書き換える」
    という違いがあります。

サンプルプログラム

同じビット列を signed と unsigned で表示して、右シフト結果の違いが出る可能性を観察するプログラムです。

ポイント

  • 同じ 0xF0(上位が1になりやすい値)を signed char と unsigned char として解釈させる
  • 右シフトしたときに、上位が 0 埋めになりやすいか、1 埋めになりやすいかを見比べる

プロジェクト名:chap7-18-1 ソースファイル名:chap7-18-1.c

// 論理シフトと算術シフトの「雰囲気」を観察する例
#include <stdio.h>

void print_bits8(unsigned x)
{
    for (int i = 7; i >= 0; i--) {
        putchar(((x >> i) & 1U) ? '1' : '0');
    }
}

int main(void)
{
    unsigned char u = 0xF0;        // 11110000
    signed char   s = (signed char)u;

    printf("観察:同じ8ビットでも、型で右シフトの結果が変わることがあります。\n\n");

    printf("unsigned char u = 0xF0\n");
    printf("u          = "); print_bits8(u); putchar('\n');
    printf("u >> 1     = "); print_bits8((unsigned)(u >> 1)); putchar('\n');

    putchar('\n');

    printf("signed char s = (signed char)0xF0\n");
    printf("s(ビット)   = "); print_bits8((unsigned char)s); putchar('\n');
    printf("s >> 1(ビット)= "); print_bits8((unsigned char)(s >> 1)); putchar('\n');

    printf("\nメモ:signed の負数右シフトは、環境によって埋め方が変わる可能性があります。\n");
    printf("ビット操作は unsigned を基本にすると安全です。\n");

    return 0;
}

このプログラムの説明

  • u は unsigned char なので、右シフト時に 上位が 0 で埋まる動き(論理シフトっぽさ)を期待できます。
  • s は signed char で、0xF0 は多くの環境で負数扱いになりやすいので、右シフト時に 上位が 1 で埋まる動き(算術シフトっぽさ)が出ることがあります。
  • ただしここが重要で、signed の負数右シフトは処理系依存なので、結果は環境で変わり得ます。
    「変わり得ることを観察する」ための教材プログラムです。

まとめ:迷ったらこう覚える

  • 論理シフト:上位を 0 で埋める(ビット列として自然)
  • 算術シフト:上位を符号ビットで埋める(符号を保ちやすい)
  • C言語で安全にビット操作したいなら、まず unsigned を使う