ビット演算



バイトとビット

 これまで,何回か主記憶(メモリー)の構造を図をつかって説明してきました.
主記憶は,1バイトのメモリーが1列に連続的に並んだもので,1バイト単位にアドレスがついていると説明してきました.また,int型の整数やfloat型の実数は4バイト,double型の変数は8バイトで表現されると説明してきました.(整数は4バイトでない場合もありますが).
CPUがデータを読み書きするときは,バイトを単位に読み書きします.しかし,この1バイトのメモリには,更に微細な構造があります.すなわち,1バイトのメモリーはより細かく見ると8個のセルで構成されています.図で表現すると次のようになります.
 
 
                       
ビット   7    6    5   4    3   2    1    0

この1個のセルのことをビットとよびます.ビットは記憶の最小単位で,1または0(つまり2進数)を記憶します.これら,8個のビットは切り離すことはできないと考えて下さい.CPUはメモリーのデータを8ビットまるごと(言い換えれば,1バイトを単位として)読み書きします.また,主記憶のアドレスというのは,この8ビットで構成される1バイトづつに割り当てられた番号ということになります.

 ついでに,左端のビットのことを最上位ビット,右端のビットを最下位ビットとよぶことも覚えておいて下さい.また,最下位ビットを第0ビット,最上位ビットを第7ビットと番号をつけて呼ぶこともあります.(最上位ビットを8,最下位ビットを1と番号付けする人もいます)



ビットパターン

 1ビットは,0または1を記憶するとのべましたが,いま,8ビットすなわち1バイトで数値を表現することを考えてみましょう.
次に示すように数値を正の整数に限れば0〜255までの256通り(2の8乗とおり)の数値が表現できます.データを下の図のように1と0で表現したものを「ビットパターン」と呼ぶことも覚えておいて下さい,
 
 
 
10進:0
10進:1
10進:2
10進:3
10進:4
10進:5
10進:6
・・・  
10進:255



ビット演算 その1 シフト演算

 次に,10進数の1,2,4,8,・・・,128の場合についてビットパターンを表示してみると,次の図のように表すことができます.
 
10進:0
10進:1
10進:2
10進:4
10進:8
10進:16
10進:32
10進:64
10進:128

 これらの数値の間には次のような関係があります.2は1の2倍,4は2の2倍,8は2の2倍,16は8の2倍,32は16の2倍,64は32の2倍,128は64の2倍となります.
 これらのビットパターンを見比べて下さい.そうです.2倍される度に1が左へ1つづつビットシフトすることが分かると思います.
 逆に,例えば,128から始めて,次々に右へビットをシフトさせると,1/2づつ小さくなっていることが分かると思います.
そうです.
数値を2倍するには,ビットを左へシフトし,1/2するには右へシフトさせればよいのです.この演算は,CPUによりますが通常の乗算(*)除算(/)で計算するより,ン十倍も計算が速いのです.(だからといって,数値計算などで使うためにあるのではありません!もちろん使えるならどこで使っても構いませんが)

 C言語では,このシフト演算を表すための,演算子が用意されています.CPUそのものにもそのような演算をする機能が備わっています.
シフト演算子は演算記号(<<)や(>>)で表現します.その使い方は直感的に分かるように>>が右シフト演算,<<が左シフト演算の記号です.

例えば,
1バイトの数値を記憶する変数aがあったとしましょう.それには1が記憶されているとします.1はビットパターンで「00000001」ですね.
  b=a<<2
と書けば,aのビットを2回左へシフトさせることとなり,bには,ビットパターン「00000100」が代入されます.

また,bに128(「10000000」)を代入し
  b=a>>4
と書けば,aの各ビットを4回右へシフトさせることとなり,bにはビットパターンは「00001000」が代入されます.すなわち10進数の8ですね.

上の2つの式では,aのビットパターンそのものは,もとのままです.もし,aのビットパターンそのものを変えたければ,複合演算子を使って,それぞれ次のように書けばよいのです.
  a<<=2
  a>>=4



ビット演算 その2 論理積(&)と論理和(|)

 これは,計測器を制御したり,入出力関連のハードウェアを直接制御するプログラムを作成するときよく使われます.(このとき使用するライブラリー関数にはoutp()関数やinp()関数などがあります.参考書などで調べてみて下さい).あっ!それから,グラフィックスのデータを処理するときなどにもよく使われます.それから,通信などでもよく使われます.初心者の人には,あまり縁がない演算かもしれません.

 まず,演算そのものをビットパターンを使って説明しましょう.



1)ビットごとの論理積【and】(演算記号 &)

 演算のルール: 1&1=1, 1&0=0, 0&1=0, 0&0=0
        (&演算子の作用は,代数演算の積と似ていますね)
 値の代入された変数aとbとのビットごとの論理積を求めるには,このルールを,変数aと変数bの対応するビットどうしに適用すればよいのです.

 c=a&b
 
変数 a
変数 b
変数 c=a&b



2)ビットごとの論理和【or】(演算記号 |)

 演算のルール: 1|1=1, 1|0=1, 0|1=1, 0|0=0
       (|演算子の作用は,代数演算の和とにてますね.ただし,1|1が1となるところだけが異なります.)

値の代入された変数aとbとのビットごとの論理和をもとめるには,このルールを変数aと変数bの対応するビットどうしに適用すればよいのです.

 c=a|b
 
変数 a
変数 b
変数 c=a|b



ビット演算 その3 どのように使われるのか

 通常のデータでは,1つの文字や数値を表すのに1,2,4,8,・・・バイトを単位に1つのデータを表現します.
ところが,ハードウェアを直接操作するプログラムでは,ビット単位で情報を表現することがあります.

 例えば,通信でおなじみのRS−232Cインターフェースでは,文字の送受信をする場合,データとしての文字は8(または7ビット)で表し,1文字づつ送受信します.
 しかし,インターフェースそのものは,有限の時間でデータを送受信するので,CPUが自己のペースで,文字を送り続けることはできません.複数の文字を連続的に送信するには,1文字送る度にインターフェースの状態,すなわち,いま,インターフェースがCPUからの文字を受け取って外部へ送信できる状態にあるかどうか確かめながら,1文字づつ送らなければなりません.これをハンドシェークとよびますが,これは,RS−232Cに限らずハードウェアレベルでデータをやりとりするときの一般的な方法なのです.
 このようなとき,インターフェース内部の状態(あるいは,インターフェースの現在の状態)を知らせるメッセージが必要となります.このメッセージはインターフェースのもつメモリー(このような場合,メモリーをレジスターと呼びます)に書き込まれます.CPUは,そのレジスターの内容をチェックし,外部への送信が可能である場合に文字をインターフェースへ渡します.
 この場合,インターフェースは,文字を「送信できる状態にある」か「送信できない状態にある」か2種類の状態を表現する必要があるわけです.2種類の状態を表現するには「1ビット」で十分です.
また,CPUがインターフェースから文字を受け取るときも,文字が受信できたかどうかレジスターをみて,文字が受信されたときデータを受け取りにいきます.この場合も文字が「受信できた」か「受信できてない」かの2通りのメッセージしか必要ないので状態は「1ビット」で表現可能です.

 このようなとき,インターフェースの内部の状態をあらわすための8ビットのレジスター(メモリ)を次のように使います.
 
レジスタ
・・ ・・ ・・ ・・ ・・ ・・ Rx Tx

ここで,Rxのビット:データが受信されたとき「1」,データが受信されてないとき「0」
    Txのビット:データ送信可能のとき「1」,データ送信不可のとき「0」
をあらわすとします.
 CPUは文字を受信するときは,Rxのビットのみをチェックすればよいわけですが,1ビットだけ読みとることはできないので,一旦,変数bに1バイト全部を読み込み,2ビット目だけ1の変数aと論理積を求めます.その結果,すなわち変数cの内容が0でなければ,Rxのビットが1,すなわちインターフェースに文字が到着ということになり,そこへCPUが文字を読みとりに行くということになるのです.このことを次の図に示します.また,もし,文字が受信できてなければRxのビットは0ですからcも0ということになります.
 
変数 a
変数 b
変数 c=a&b

 文字を送信するときは,Txのビットを同様にチェックします.ただし,この時は,当然,変数aに「00000001」を代入しておかなければなりません.

 レジスターでは,2ビットまとめて3,または4種の状態を表現(あるいは,3ビットまとめて7ないし8種の状態を表現したり・・・・)する場合もあります.

□ここでは,論理演算の用途の1例のべただけですが,いろいろなプログラムを組んでいるうちに実際に遭遇することになるでしょう.
□画像処理などにもよくつかわれます.
 



ビット演算 その4 XORとビット反転

 ビット演算には,その他にXOR(演算記号^)やビットを反転(~)させる演算があります.
演算のルールのみ記しておきます.



1)ビットごとのXOR(演算記号 ^)

 演算のルール: 1^1=0, 1^0=1, 0^1=1, 0&0=0
        (お互いのビットが異なるとき1,お互いのビットが同じとき0)



1)ビット反転(演算記号 ~)

 演算のルール: ~1=0, ~0=1
       (~aと書けば,変数aに記憶されたすべてのビットを反転させます)
 


ビット演算を活用する

ビット演算は初心者にはなじみの薄いものです。しかし、一度使い方を覚えて使い出すと病み付きになるくらい便利であり効率的でもあります。

ここでは実例をまじえつつビット演算を活用する方法について説明していきたいと思います。
まず、ビット演算を利用するメリットを挙げますと・・・

(1)演算速度が高速である
(2)メモリの消費量を最小限に抑えることが出来る
(3)1つの変数で複数の情報を持つ事が出来る

などがあります。(1)については既に言いましたが、コンピュータが唯一理解することが出来る「機械語」のレベルでその計算命令数の差を見てみるとその速度差は歴然です。
また(2)と(3)は同じような意味合いですが、1つの情報をバイト単位ではなくビット単位で扱うことでより多くの情報を変数に詰め込むことが出来ることから言えます。

では、例として「1バイトの変数で指示を与え「画面上の決まった位置に図形を書く」プログラムを考えてみます。

まず、描画用の関数を次のように宣言します。

int Draw_Object(unsigned char mode);

mode 変数には書く図形の情報をビット単位で詰め込むことにします。
そして、この図形には次のような8つの状態があるとします。

三角形

線を点線にする
線を黒色にする
線を赤色にする
黒く塗りつぶす
黄色く塗りつぶす
透明にする

それぞれに1バイト変数のうち1ビットを割り当てます。
割り当ては次のようにします。
 
第nビット
持たせる情報 三角形 線を点線にする 線を黒色にする 線を赤色にする 黒く塗りつぶす 黄色く塗りつぶす 半透明にする

次に、各状態とビット位置を簡単に関連付けるために #define 構文を使って次のように定義します。(0x**とは、16進数で**という意味です。ビットを扱う時は10進数ではなく16進数で考えるのが自然です。もちろん10進数でも問題はありませんが。16進数で考えると、各ビットは8,4,2,1という4つのキーワードの組み合わせで考えることが出来ます。)

#define SQUARE          0x80  /*2進数では【10000000】*/
#define CIRCLE            0x40  /*【01000000】*/
#define DOT_LINE         0x20  /*【00100000】*/
#define LINE_BLACK     0x10  /*【00010000】*/
#define LINE_RED         0x08  /*【00001000】*/
#define BG_BLACK       0x04  /*【00000100】*/
#define BG_YELLOW     0x02  /*【00000010】*/
#define BG_SKELETON  0x01  /*【00000001】*/

これで、文字により状態を設定する環境が整いました。
ん、これはどういうことでしょうか?例えば「円で、かつ線が点線で、かつ黒く塗りつぶす」といった場合、ビットの状態を考えると【01100100】となります。これを16進数で表すと0x64となります。この値はどうやって求めるのでしょうか???プログラミングの時にいちいち電卓を叩くのも気が引けます。しかし、文字の組み合わせで次のように表現できるとしたら簡単だと思いませんか?

「CIRCLEで、かつDOT_LINEで、かつBG_BLACKである」

これが、#define とビット演算を使って簡単に実現できます。
上記の3つのオプションをmode変数に指定するには次のように記述します。

mode = CIRCLE | DOT_LINE | BG_BLACK;

これだけです。|は以前出てきた論理和の演算子(OR演算子)です。
この場合、内部では
mode = 0x40 | 0x20 | 0x04;
つまり
【01000000】or
【00100000】or
【00000100】 = 【01100100】という演算を行うことになります。

冒頭で出てきたDraw_Object関数を実際に利用する時は例えば次のように書きます。

Draw_Object(CIRCLE | DOT_LINE | BG_BLACK);
Draw_Object(CIRCLE | DOT_LINE | LINE_BLACK | BG_YELLOW);
Draw_Object(SQUARE);
Draw_Object(CIRCLE | BG_SKELETON);
Draw_Object(0); /*何も書かない*/

このように、ビットとして扱うことで簡単に関数への複数指示を行えるようになります。
つぎに、命令を受け取った関数内部ではどのように処理したらいいのかを検証します。

Draw_Object(unsigned char mode)
{
   ・・・
   ・・・
}

引数modeには基本的に複数の情報が詰め込まれています。このmode変数から各ビットの情報を取り出すにはどのようにしたらいいでしょう?
難しそうですが、実際には非常に簡単です。

result = mode & 【調べたい桁】;

とすることで、result変数には0か、0以外が入ります。
例えば、

result = mode & CIRCLE;

となっていたとします。mode = CIRCLE | DOT_LINE | BG_BLACK のとき、

【01100100】and
【01000000】 = 【01000000】という演算結果になり、非ゼロとなります。同様にmode = SQUARE | DOT_LINE | BG_BLACK のとき

【10100100】and
【01000000】 = 【00000000】という演算結果になりゼロとなります。

ゼロになるかならないかで、そのビットが0か1かを判断できます。
また、2つの状態(すなわち2ビット以上が1)が同時に起こっているかを調べたい場合はゼロ・非ゼロの判断基準ではなく、or演算子で結んだ状態と等しくなるかを調べます。
例えば、mode = CIRCLE | DOT_LINE | BG_BLACK と仮定して、

result = mode & (CIRCLE | BG_BLACK);
とすれば、CIRCLE | BG_BLACKは
【01000000】or
【00000100】 = 【01000100】という演算結果になります。この結果とmodeのand演算を行うと、

【01100100】and
【01000100】 = 【01000100】となります。この値はCIRCLE | BG_BLACKと等しくなります。プログラム中ではこの動作を
if((mode & (CIRCLE | BG_BLACK)) == (CIRCLE | BG_BLACK))
{
   ・・・
}
というような文で表現します。

上の例でmode = SQUARE | DOT_LINE | BG_BLACKのときはどうなるでしょうか?
【10100100】and
【01000100】 = 【00000100】となります。これは(CIRCLE | BG_BLACK)とは異なるため、2つのビットが同時に1になっていないと判断できます。

 まとめとして簡単なプログラムを作ってみます。
/* ビット演算サンプルプログラム その1         */
/* by Naoto Fujiwara 2000/7/1 naonao@wombat.or.jp */

#include <stdio.h>
#include <string.h>

/* (注)プログラム中に出てくるstrcat 関数は文字列バッファの最後に指定した文字を追加する関数です */
/* (例)buffer = "abcd"; strcat(buffer,"ABCD"); ->  bufferは"abcdABCD" となります*/

/* 各ビットに対応する文字表記可能なオプション指示定数を定義 */
#define PRINTF_FLOAT       0x80
#define PRINTF_INT        0x40
#define PRINTF_TAB        0x20
#define PRITNF_SPACE    0x10
#define PRINTF_COLOR_RED   0x08
#define PRINTF_COLOR_GREEN  0x04
#define PRINTF_SHOW_MYNAME 0x02

/* サンプルのデータ表示関数 */
int sample_printf(double data,int mode);

int main(void)
{
 /* 表示のテスト */
 sample_printf(3.14159,PRINTF_FLOAT);
 sample_printf(3.14159,PRINTF_INT);
 sample_printf(3.14159,PRINTF_FLOAT | PRITNF_SPACE | PRINTF_COLOR_RED);
 sample_printf(3.14159,PRINTF_FLOAT | PRINTF_COLOR_GREEN | PRINTF_SHOW_MYNAME);
 sample_printf(3.14159,PRINTF_FLOAT | PRINTF_TAB | PRITNF_SPACE | PRINTF_SHOW_MYNAME);
 sample_printf(3.14159,PRINTF_INT | PRINTF_FLOAT);
 sample_printf(3.14159,PRINTF_INT | PRINTF_TAB | PRINTF_COLOR_RED | PRINTF_SHOW_MYNAME);
 sample_printf(3.14159,PRINTF_INT | PRINTF_TAB | PRITNF_SPACE);
 return 0;
}

int sample_printf(double data,int mode)
{
 /* このバッファに数々の書式を入れていき、最終的にprintf関数に渡します*/
 char print_buffer[100];

 /* mode変数の値を16進数表示 */
 printf("mode = 0x%2X : ",mode);

 /* 標準の文字をバッファに書き込む */
 strcpy(print_buffer,"表示結果−>");

 if((mode & (PRINTF_INT | PRINTF_FLOAT)) == (PRINTF_INT | PRINTF_FLOAT))
 {
  printf("整数型と浮動小数点型を同時に指定することは出来ません\n");
  return -1;
 }

 /* 0になると、どちらのオプションも設定されていない事を意味します。 */
 /* ちなみに、下のif文は、if(!(mode & (PRINTF_INT | PRINTF_FLOAT)))  */
 /*    { ・・・ }                                         */
 /* と記述するのがベストです。                                       */
 if((mode & (PRINTF_INT | PRINTF_FLOAT)) == 0)
 {
  printf("整数型か浮動小数点型のどちらかを指定する必要があります\n");
  return -1;
 }

 /* 文字に色をつけるオプションが設定されている場合です。                          */
 /* このif文も、if(mode & PRINTF_COLOR_RED) [ ・・・ } と記述するのがベストです。 */
 if((mode & PRINTF_COLOR_RED) != 0)
 {
  strcat(print_buffer,"\x01b[31m");/* 赤色にする命令。エスケープシーケンスといいます(^_^; */
 }
 else if((mode & PRINTF_COLOR_GREEN) != 0)
 {
  strcat(print_buffer,"\x01b[32m");/* 緑色にする命令 */
 }

 /* タブを入れる */
 if((mode & PRINTF_TAB) != 0)
 {
  strcat(print_buffer,"\t");
 }

 /* スペースを入れる */
 if((mode & PRITNF_SPACE) != 0)
 {
  strcat(print_buffer,"   ");
 }

 /* 整数型表示のオプションが設定されている場合です。                                       */
 /* このif文も、if(mode & PRINTF_INT) [ ・・・ } と記述するのがベストです。                */
 /* ちなみに、引数がdouble型のため期待通りの表示は行えません(笑)あくまで実験ですので・・・ */
 if((mode & PRINTF_INT) != 0)
 {
  strcat(print_buffer,"%d");
 }
 /* 浮動小数点型のオプションが設定されている場合 */
 else
 {
  strcat(print_buffer,"%f");
 }

 /* 名前を入れる */
 if((mode & PRINTF_SHOW_MYNAME) != 0)
 {
  strcat(print_buffer,"  naonao");
 }

 /* 最後に標準で改行を入れる */
 strcat(print_buffer,"\n");

 /* 表示を行う */
 printf(print_buffer,data);
 return 0;
}

実行結果は自分の目で確かめてください(^_^)

あと、補足ですが上のプログラムで次のような操作もできます。

int mode;
mode = ~PRINTF_FLOAT;
こうすれば、PRINTF_FLOAT以外の全てのオプションを設定することになります。
つまり、
mode = PRINTF_INT | PRINTF_TAB | PRITNF_SPACE | PRINTF_COLOR_RED | PRINTF_COLOR_GREEN | PRINTF_SHOW_MYNAME;
と指定したのと同じ意味になります。

また、
mode = PRINTF_INT | PRINTF_TAB | PRITNF_SPACE;
のとき、
mode &= ~PRINTF_TAB;
とすると、PRINTF_TABを取り消したことになります。すなわち
mode = PRINTF_INT | PRITNF_SPACE;
と等しくなります。

これをビットを使って解説しますと・・・

PRINTF_INT | PRINTF_TAB | PRITNF_SPACE は2進数では【01110000】
また、 mode &= ~PRINTF_TAB  は
mode = mode & ~PRINTF_TAB と解釈できますので、

mode & ~PRINTF_TAB は
~【00100000】 -> 【11011111】
∴【01110000】 & 【11011111】 = 【01010000】
すなわち
mode = PRINTF_INT | PRITNF_SPACE;
です。

ビット演算の組み合わせであらゆる表現が出来るように頑張りましょう!!