5.演算子

5-9. 2進法・ビット演算子

◆ビット

これからはもっとハードウェアに近いお話になります。
ビットについてのお話です。いままでのビットの説明は、

半角1文字=1バイト=8ビット

という説明だけでした。
これだけでは情報の容量の単位でしかわからないですね。

そもそもビットとは何を表しているのでしょうということからはじめます。

ビット(bit:binary digit)は2進数字の略です。具体的に0と1の2進法で使われる2つの数字のことです。
コンピュータではこの1と0を電気的に記憶することからメモリの記憶容量の単位にも使われています。
またプログラムではビットは計算上2進数の桁数としても使われます。
(ハード的にはレジスタですが)

◇2進法とは

2進法とはズバリ0と1だけで計数することです。

これだけでは、意味が通じないので生活上のほかの例で行きましょう。

まずは、ふだん使うアラビア数字0~9は10進法です。
10になると桁が1つ増えます。

そして、時間は12(24)進法と60進法の両方を使います。(注:秒以上)
60分で1時間、12時間で午前・午後が変わり、24時間で1日です。

タバコとビールは20進法です(笑)、1箱が20本ですから…

鉛筆などは12進法です。(12本=1ダース)

以上の例からコンピュータでは普段使ういろいろな計数法をすべて2進法に変換して操作しているのです。(時間も2進法で計算するからスゴイ!)

100分が1時間40分と変換するような要領で2進数を10進数にしてみましょう。

2進数 10進数
  0    0
  1    1
 10    2
 11    3
 ・     ・
 ・     ・
111    7

2進数では0と1しか使えないのでどんどん桁上がりしているのがわかりますね、

◆2進数使うのは何故?

さて、コンピュータが2進数なのは何故でしょう?

-デジタルだから?アナログじゃないから….

確かにそうですがそもそもデジタルって…

デジタル:計数型
アナログ:計量型

デジタルとアナログの違いは上のようになります。

もちろん、今のコンピュータは電気的にデジタルで処理しています。
そうすることによって、電気ノイズによる計算間違いが起こらないようにしています。
(デジタルだとノイズの影響がないとは言えませんがアナログに比べ大変ノイズに強いです)

しかも2進数であるのは中途半端は値を許さないからです。
(白黒ハッキリしてるわけね…)

メモリは電気的に情報を記憶しますが、もっと詳しく言うとメモリ内部のトランジスタにかける電圧によって0と1を表現します。

これがもし10進数で格納するとしたら電圧を10段階に分けてかけなければならないのでコンピュータにとって煩雑になります。
(多分、回路設計者は大変になるでしょう)

もちろん、計数間違いの危険性も高くなります。

ところでアナログですがこちらも利点があります。理論上、精度が無限大であることです。あと計量的なのでい一般にデジタルより計算は高速になります。

ただし、ノイズに弱いのでノイズ対策が甘いと常に誤差を出します。

1 + 1 = 2.00000000000000000000000000437467

みたいな事が常に起こるわけです。その点デジタルは情報の劣化(AD変換などを除く)が起こらないので情報分野では重宝されます。

また、年々デジタルはより高精度に、より高速に発展しているので速度的にも
アナログでないと無理だったテレビの受信もできるようになりました。

というわけで、パソコンは2進数なのです。
コンピュータのためにも2進数を使うことを理解してあげてください(自爆)

◆ビット演算子

ビット演算子とはこれまでの演算子と違い数値をビット単位で論理演算するものです。

なのでビット演算子をつかうには10進数を2進数で表した時の数値を理解してないとダメです。

ずっと前にintは16ビットまたは32ビットのデータ長と説明しましたが、これは2進数で表した時の最大桁数になります。
(ここでは絶対値で言っています)

また数値より上の桁は0で埋められます。
10なら、00000000000000000000000000001010 です。

ここでは、演算子の説明だけに絞りたいのでデータ長は8ビットで扱います。

ビット演算子には次の種類があります。

  AND:&
   OR:|
  XOR:^
  NOT:~
 LEFT:< >

AND・ORの真理値は前に論理演算子で説明しましたが、今回排他的論理和(XOR)という新しいものが出てきています。

XORの真理値は次にようになります。

A B XOR
0^0 0
0^1 1
1^0 1
1^1 0

AとBの値が異なると1となります。

これらビット演算子はビット単位(各桁)で演算するので、

6&13 とすると、

   6:0110
& 13:1101
─────────
   4:0100

となります。

6|13ならば、

   6:0110
│ 13:1101
─────────
  15:1111

となります。

また~はビットの反転を行うので

^6は

 ~6:0110
────────
  9:1001

になります。

そして、LEFTとRIGHTはそれぞれ左右にビットをシフトします。

6<<1 6を1ビット左シフト

    6:0110
      ////
   12:1100 (右端に0が追加される)

6>>2 6を2ビット右シフト

    6:0110
      \\\\\
       \\\\\
    1:0001   (左端に0が追加される)

また、シフトによってはみ出したビットはその情報が消滅してしまいます。

なお、負の整数のシフトについては規定がなくこれもコンパイラによって処理が異なることがあるのでそこまで突っ込みません(前回の反省より)

◆論理演算子の罠

論理演算子のついて今までは関係演算子とともに使っていましたが、それ以外の場合について説明します。

今までは、2<i && i<9 のような使い方をしていました。

それでは i && 9 としたらどうでしょう。ここではじめて0と1以外の値が真偽の判定に使われました。

結論から言いますと0以外の値は真として見なします。
内部的には(zero flag)を検知しているのでしょう。

上の論理式の場合 9 は恒真なので i が0であれば偽、それ以外は真になります。

次に、算術演算を含んだ論理式はどうでしょうか?

1+1 && 3-2-1+7

のようなものです。これはもちろん算術演算された結果を判定に使うので、

2 && 7

と同じ意味で真になります。代入演算式も含むことができます。

たとえば、x = 1 && y = 2

このような使い方ができるということです。
しかし! ここに罠がありました!

論理積の場合、1番目の式が偽であれば2番目がどんな値を取ろうとも偽になります。そして高速化のため2番目を省略してしまうのです。

x = 0 && y = 2

という式があった時 x = 0 の時点で論理式が偽になることがわかるので y = 2 が省略されて実行されないのです。

そして論理和の場合1番目が真ならば必ず式全体が真になるのでこのとき2番目が省略されます。

x < 2 || y = x + 2

もし x < 2 が真ならば右側の代入が行われなくなります。
というわけでくれぐれも論理式内での代入演算は気をつけましょう。

◆負の2進数の表現

ビット演算を行うのあたって2進数の負の表現を理解しておく必要のあるところがありましたので、ここで紹介させていただきます。

ここでは説明を簡略化するため4ビットの2進数を利用します。

通常4ビットで数値を表現しようとすると(数値でなくてもいいが)16種類の値が表現できます。これを0~15の数値に割り当てて使っています。

しかし、これでは負の数を表すことができません。そこで16種類のうち半分の8種類を負の数に割り当ててみましょう。

0 0000
1 0001
2 0010
3 0011
4 0100
5 0101
6 0110
7 0111

とここまでは、一緒です。

 正  負
 8 -8 1000
 9 -7 1001
10 -6 1010
11 -5 1011
12 -4 1100
13 -3 1101
14 -2 1110
15 -1 1111

ここで正負の表現が変わりました。

ポイントとしては、
1.左端のビットが1である。
2.負の数を増分していくと0で一致するのでコンピューターにとって計算がラク。

ということです。2についてもう少し説明しますと、

-3+7=4 は

  1101
+ 0111
──────
 10100 5ビット目は溢れて無視されるので 0100=4

2-5=-3 …2+(-5)=-3 として計算

  0010
+ 1011
──────
 11101 5ビット目を無視して 1101=-3

こんな感じでコンピュータ側には正負の意識の必要がないようになっています。
それだけでなく減算も加算として計算できるメリットがあります。

また左端のビットの状態により正負の判断ができるので、負の数の出力ができます。

で、これでは人間にとってはわかりにくいでしょう。(もともと二進だし)

ということで、負の2進数の求め方をちょっと紹介しましょう。

方法1:忠実な方法

たとえば-3を表現したいと思います。
符号キーのついてない電卓ではどうしますか。
0-3=-3 ってしますね。
これと似たようなことをやってみます。
データ長が4ビットの場合なら
0000-0011
まだこれだとわかりにくいので5桁目に1があると仮定して
10000-0011 とします。
(右4桁の状態が同じなので結果に変化はありません)

それを10進数で計算すると
16-3=13 となります。結果の4ビットは同じなので。

-3は正の13の状態と同じです。結果は1101になりました。

方法2:メジャーな方法

こちらは多くの人が使っているメジャーな方法です。

やり方は簡単ビットを反転して1を足すのです。
-3を表現するには3を反転して1を足すのです。

0011─(反転)→1100─(+1)→1101

めちゃ簡単でしょ。(おおっ!)

これは2つとも『補数』の理論を実用化したものです。
補数の理論まで書いちゃうと『第二種午前対策』になっちゃうし、ここでは、負の数は左端のビット1であることだけを説明したかったので、詳細は割合させていただきます。

◆ビット演算子の技

さあ、ここではビット演算子の定番の技を紹介します。

1.マスク

最近、家でもLANをされる方が増えましたが、ネットマスクとか言って255.255.255.0 なんて値を見たことがあるかと思います。

マスクとはビット列の内の1部分を0に初期化する働きがあります。
マスクではAND(論理積)を利用して初期化したいビットに0を入れます。

たとえば、1101010010010111 というビット列があります。これを下位8ビットをマスクしてみます。(C言語上では16進数表現で使います)

0xd497 & 0xff00

0xd497 1101010010010111
0xff00 1111111100000000
─────────────
0xd400 1101010000000000 下位8ビットが初期化されました。

-ところで、ネットマスクは何につかってるの?
ネットマスクはネットワークアドレス(先頭アドレス)とネットワークの範囲の特定に使われます。つまり、初期化した時のアドレスが先頭アドレスになり、初期化されて桁数がネットワークの範囲(IPの範囲)になります。

自分のIPアドレスが 192.168.128.133 とします。
それで、ネットマスクが255.255.255.0   のときは

192.168.128.0 がネットワークアドレスになり初期化されたビット数は8なのでネットワークアドレスを先頭に256個のIPがその範囲になります。
つまり192.168.128.0~255が1つのネットワークとなります。

2.セット

今度は1とは逆に指定したビットを1に初期化します。
(マスクはリセットしている)

方法は演算子をOR(論理和)使い、セットしたいビットに1を代入します。
ちょうどマスクと逆ですね…

0xd497 | 0xff00

0xd497 1101010010010111
0xff00 1111111100000000
─────────────
0xd400 1111111110010111 上位8ビットがセットされました。

3.反転

反転はXOR(排他的論理和)を使います。
反転させたいビットに1を代入します。

0xd497 ^ 0xff00

0xd497 1101010010010111
0xff00 1111111100000000
─────────────
0xd400 0010101110010111 上位8ビットが反転されました。

4.シフトについての注意!

ビットシフトには『論理シフト』と『算術シフト』の2種類が存在します。

論理シフト:単純にビットを移動する。符号の保証がない。
算術シフト:符号ビットを保証するシフト演算、数値の2倍、半分などの演算が符号による不具合なしにできる。

これらはどのように区別するのかは、変数の宣言にみればわかります。
unsigned(符号なし)で宣言されていれば論理シフトになります。
もちろん符号がないのでシフトによる符号の反転が起こらないため問題ありません。

そして符号付の変数の場合シフト演算によって符号が反転しないように、左端のビットの値は固定されます。また右シフトしたとき左端のビットの値が代入されていきます。これが算術シフトです。