C言語

ビット演算・シフト演算

プログラムでは、数値をビット(0と1)で表しています。
C言語では、このビットを直接操作できる「ビット演算」と「シフト演算」が用意されています。
今回は、そのビット演算とシフト演算の基本的な使い方を説明します。

ビット演算とは

ビット演算とは、0と1の並び(ビットの集まり)を直接操作する方法 のことです。
1つ1つのビットに対して、「同じ位置のビットどうしで計算する」ようなイメージです。

例えば、次のように2つの数があったとします。

a = 1100
b = 1010

この2つのビットを、1列ずつ比較して結果を作るのがビット演算です。

例えば、次のような2つの数があるとします。

a = 1100(=12
b = 1010(=10

この2つの数を「ビットごと(1列ずつ)」に比べて、0と1の組み合わせに応じて計算していくのがビット演算です。

ふつうの演算(+、−、×、÷)は「数」そのものを計算しますが、ビット演算は「0と1の並び」を1つずつ比べて処理します。
ビット演算には、AND(&)、OR(|)、XOR(^)、NOT(~)を使用します。

演算子名称説明
&AND(論理積)両方が1なら11 & 1 = 1
|OR(論理和)どちらかが1なら11 | 0 = 1
^XOR(排他的論理和)違っていたら11 ^ 1 = 0
1 ^ 0 = 1
~NOT(否定)反転~1 = 0

それぞれのビット演算子の例です。

&演算子(AND)の例

両方が1のときだけ1になります。

unsigned char a = 0b1100; // 12
unsigned char b = 0b1010; // 10
unsigned char c = a & b;  // 1000 → 8
ab結果(c)
111
100
010
000

つまり、「共通して1のところ」だけを取り出します。
このため、特定のビットを取り出すときに使われます。

|演算子(OR)の例

どちらかが1なら、結果も1になります。

unsigned char a = 0b1100; // 12
unsigned char b = 0b1010; // 10
unsigned char c = a | b; // 1110(=14)
ab結果(c)
111
101
011
000

「どちらかがONならONになる」ようなイメージです。
複数の条件をまとめてONにするときに使います。

^演算子(XOR)の例

同じなら0、ちがっていたら1になります。

unsigned char a = 0b1100; // 12
unsigned char b = 0b1010; // 10
unsigned char c = a ^ b; // 0110(=6)
ab結果(c)
110
101
011
000

「スイッチを反転させる」ようなときに使えます。

~演算子(NOT)の例

すべてのビットを反転させます。

unsigned char a = 0b00001111;
unsigned char b = ~a; // 11110000

0は1に、1は0に入れ替わります。

シフト演算とは

シフト演算は、ビットを左右にずらす演算です。
0と1の並びをずらすことで、数の大きさが変わります。

名前演算子説明
左シフト<<
左にずらす(2倍ずつ大きくなる)
右シフト>>
右にずらす(1/2ずつ小さくなる)

<< 演算子(左シフト)の例

左にビットをずらすと、空いた右側のビットには0が入ります。

unsigned char a = 0b00000010; // 2
unsigned char b = a << 1;      // 0000 0100 → 4

1ビット左にずらすと「2倍」になります。
つまり、「左にn回ずらすと 2のn乗倍」になります。

結果
1 << 12(0b10)
1 << 24(0b100)
1 << 38(0b1000)

>> 演算子(右シフト)の例

右にビットをずらすと、左側に何が入るかがポイントです。

unsigned char a = 0b00001000; // 8
unsigned char b = a >> 2;      // 0000 0010 → 2

右に1つ動かすと1/2になります。
ただし、「左の空いたビット」に入る値は型によって異なります。

種類空いたビットに入る値呼び方
unsigned(符号なし)の型0が入る論理シフト
signed(符号あり)の型符号ビット(最上位ビット)の値が入る算術シフト
符号なし(unsigned)の右シフトの例
unsigned char a = 0b11111000; // 248
unsigned char b = a >> 1;     // 0111 1100 → 124

unsignedが付いた型の右シフトは、空いた左のビットには常に0が入ります。
これが、論理シフトです。

符号あり(signed)の右シフトの例

符号あり(signed)の場合は、符号ビット(最上位ビット)の値が入るため、このビットが、0 なら、シフトした後に空いたビットには、0 が入ります。
符号ビット(最上位ビット)0 なら、シフトした後に空いたビットには、1 が入ります。
つまり、ゼロとプラスの値の場合は、シフトした後に空いたビットには、0 が入り、マイナスの値の場合は、シフトした後に空いたビットには、1 が入ります。

符号ビット(最上位ビット)とは、一番左側にあるビットのことです。

例えば、-8 は、8ビットで次のようになります。

1111 1000


これを右に1ビットシフトすると、

1111 1000 >> 11111 1100(-4

左のビット(最上位ビット)が「1(符号ビット)」で埋まります。
これを 算術シフト といいます。 負のまま半分にする動きです。

符号あり(signed)でも、負の値(マイナスの値)でない場合(プラスの値)は、符号あり(最上位ビット)は 0 です。
このため、符号ありでも、プラスの値の場合は、右シフトしても、左のビット(最上位ビット)は「0(符号ビット)」で埋まります。

符号ありの 8 は、8ビットで次のようになります。

0000 1000 >> 10000 01004

2の補数とは

-81111 1000 となっていることに疑問が出てきました。
なぜ、-8 が2進数では、この値になるのでしょうか。
それは、マイナスの値を 2の補数 という方法で表すからです。
これは、マイナスの値もビットの並びだけで表す必要があるためです。

例えば、 5-5 を8ビットで表すと、

0000 0101 (5)
1111 1011 (-5)

-5 は、5 を反転して(0と1を入れ替えて)+1 していることが分かります。

つまり、2の補数とは、ビットを反転して+1しているということです。

8ビットは、256通りの組み合わせしかありません。
0000 00001111 1111
の256通りの組み合わせです。

もしこれを、「0~255」として使うと、マイナスの値が入りません。
そこで、半分をマイナスの値として使うようにします。

0000 00000111 1111: 0~127
1000 00001111 1111: -128~-1

どうすれば「足し算」でマイナスを表せるか

コンピュータは、引き算ができません。
(正確には「引き算用の回路を作らずに、足し算で引き算をしたい」)

このため、5 - 5 = 0 は、5と何を足したら 0 となるかで表そうと考えます。
すなわち、5 + (-5) = 0 と考えます。
8ビットで、5と-5を表わすと、
5 :0000 1001
-5:1111 0111

これを足すと、

0000 1001 (5)
+1111 0111 (-5)
--------------
= 1 0000 0000

となります。一番左端の1は、桁があふれているため、切り捨てられます。
このため、 0000 0000 となり、結果、0 となります。

このように、2の補数を使用することにより、足し算の仕組みだけで、ひき算ができます。

つまり、マイナスの数もプラスの数も、同じ足し算回路で処理できる のです。
そのため、CPUの構造をシンプルにできて、高速に計算できるようになります。

まとめ

  • コンピュータの中では、すべてのデータは 0と1(ビット) の並びで表されている
  • ビット演算は、数を作る0と1を「1つずつ計算」する演算
  • 主なビット演算には次の4つがある
    • AND(&):両方が1のときだけ1になる
    • OR(|):どちらかが1なら1になる
    • XOR(^):異なっていれば1、同じなら0になる
    • NOT(~):0と1を入れ替える(反転する)
  • シフト演算は、ビットを左右にずらす演算
    • 左シフト(<<):右の空いたビットに0が入り、2倍ずつ大きくなる
    • 右シフト(>>):左の空いたビットには
      • 符号なし(unsigned)なら0が入る(論理シフト)
      • 符号あり(signed)なら符号ビットが入る(算術シフト)
  • 負の数は 2の補数 という仕組みで表されており、「ビットを反転して+1」することで作る

-C言語