ビット演算の活用法
ビットとビット演算では、ビット演算の方法について説明しました。
しかし、あの説明だけでビット演算を有効に活用できる人はあまりいないでしょう。
二進数で値を操作するというのはイメージがしづらく、コードも読みづらく、メリットが見出せないと思います。
ビット演算は高速である、という特徴があります。
しかしよほど限られた状況でなければ処理のわかりやすさを優先したコードにした方が良いでしょう。
ビット演算の方が多少処理が速いとしても、最近のパソコンは十分に高性能であるためそこまで気にするほどでもありません。
組み込み系(パソコンではなく、家電等に内蔵されているソフトウェア)ではハードウェア性能が貧弱なので、今でもよく使われています。
ビット演算は他人のコードを読む際にたまに登場するので、知識として知っていて損はないし、いくつかの場面では有効に使えると思うので、(少ないながら)紹介します。
論理演算
複数のフラグ管理
フラグとは、ある状態がオンかオフかを表すための変数です。
int flag;
//フラグをオンオフする何らかの処理
//...
if(flag)
{
//フラグがオンのとき
}
else
{
//フラグがオフのとき
}
このような処理を書くことはよくあります。
これがフラグです。
例えばチェックボックスのオン/オフ状態や、特定の処理の成功/失敗などをフラグで管理します。
ゲームでは、特定の行動をしたらフラグをオン、キャラクターの状態異常は対応するステータスのフラグをオン、などがよく用いられます。
(flag=旗。旗を立てて目印にすることからフラグを立てるなどと言います)
C言語で一番小さなデータ型はchar型の1バイトです。
「オン/オフ」の状態は「1か0」、つまり1ビットあれば保存できます。
この情報をchar型変数に保存すると7ビットは無駄となります。
(1バイト=8ビット=256通りの情報量)
変数ひとつふたつ程度では問題になりませんが、フラグの数が増えると無駄が増えます。
//5ビットあれば足りるのに
//8 × 5 = 40ビット消費する
char flag1, flag2, flag3, flag4, flag5;
char型は8ビットの情報を持てるので、ビット演算を使えばフラグを8個詰め込んでしまうことができます。
#include <stdio.h>
#define FLAG1 1
#define FLAG2 2
#define FLAG3 4
#define FLAG4 8
#define FLAG5 16
int main()
{
unsigned char flag = 0;
//FLAG1とFLAG3をオン
flag |= FLAG1;
flag |= FLAG3;
printf("flag1: %s\n", (flag & FLAG1) ? "On" : "Off");
printf("flag2: %s\n", (flag & FLAG2) ? "On" : "Off");
printf("flag3: %s\n", (flag & FLAG3) ? "On" : "Off");
printf("flag4: %s\n", (flag & FLAG4) ? "On" : "Off");
printf("flag5: %s\n", (flag & FLAG5) ? "On" : "Off");
getchar();
}
flag1: On flag2: Off flag3: On flag4: Off flag5: Off
5つあった変数を、ひとつの変数にまとめてしまっています。
最初の定数の定義では1から順に倍々に数値を定義しています。
これを二進数に直すと、以下になります。
(今回はunsigned char型の変数で利用するので8ビットで表示しています)
#define FLAG1 1 //00000001
#define FLAG2 2 //00000010
#define FLAG3 4 //00000100
#define FLAG4 8 //00001000
#define FLAG5 16 //00010000
二進数ですから、2倍すると桁がひとつ上がります。
(十進数では10倍で桁がひとつ上がるのと同じ)
#define
ではなくenum
で定義しても構いません。
フラグをビット演算で管理する場合、二進数の値が十進数で何を表すかを考える必要はありません。
二進数のそれぞれの桁を、独立した「0と1の情報を持てる領域」と見立てます。
例えば「00000101」なら、「FLAG1」と「FLAG3」がオンで、後はオフという状態を表します。
char型なら8ビット(8桁)ですから、8通りの独立したオンオフ情報を持てるということになります。
int型(32bit環境)なら32ビットですから、ひとつの変数で32のフラグ管理ができます。
char型変数を32個用意するより、int型変数ひとつで管理した方が効率が良くなります。
フラグの演算方法
フラグの演算方法には以下のようなものがあります。
//フラグのオン
flag = flag | FLAG1;
flag |= FLAG1;
//複数のフラグを同時にオン
flag |= FLAG1 | FLAG2;
//フラグのオフ
flag = flag & ~FLAG1;
flag &= ~FLAG1;
//複数のフラグを同時にオフ
flag &= ~(FLAG1 | FLAG2);
//フラグの反転
flag = flag ^ FLAG1;
flag ^= FLAG1;
//フラグのチェック
if (flag & FLAG1)
printf("On");
else
printf("Off");
実際に使用したのが以下のサンプルコードです。
#include <stdio.h>
#define FLAG1 0x01 //0000 0001 1
#define FLAG2 0x02 //0000 0010 2
#define FLAG3 0x04 //0000 0100 4
#define FLAG4 0x08 //0000 1000 8
#define FLAG5 0x10 //0001 0000 16
#define FLAG6 0x20 //0010 0000 32
#define FLAG7 0x40 //0100 0000 64
#define FLAG8 0x80 //1000 0000 128
#define FLAG9 0x0100 //0000 0001 0000 0000 256
void ShowFlag(unsigned int flag)
{
printf("flag1: %s\n", (flag & FLAG1) ? "On" : "Off");
printf("flag2: %s\n", (flag & FLAG2) ? "On" : "Off");
printf("flag3: %s\n", (flag & FLAG3) ? "On" : "Off");
printf("flag4: %s\n", (flag & FLAG4) ? "On" : "Off");
printf("flag5: %s\n", (flag & FLAG5) ? "On" : "Off");
printf("flag6: %s\n", (flag & FLAG6) ? "On" : "Off");
printf("flag7: %s\n", (flag & FLAG7) ? "On" : "Off");
printf("flag8: %s\n", (flag & FLAG8) ? "On" : "Off");
}
int main()
{
unsigned int flag = 0;
flag |= FLAG1; //オン
flag |= FLAG3; //オン
flag &= ~FLAG1; //オフ
flag ^= FLAG5; //反転
flag |= FLAG2 | FLAG3; //同時にオン
ShowFlag(flag);
getchar();
}
flag1: Off flag2: On flag3: On flag4: Off flag5: On flag6: Off flag7: Off flag8: Off
最初の定数宣言では16進数で定義していますが、やっていることは最初のコード(1、2、4、8、16...)と同じです。
ビットの並びがイメージしやすいという理由からこちらを好む人もいます。
ビット演算でフラグ管理すると変数はひとつで済みますから、関数の引数にする場合でもフラグ変数をひとつ渡すだけで済んでしまいます。
(もちろんフラグの数があまりにも多い場合は変数の数も増えます)
ビット演算によるフラグ管理の実用性
ビット演算でフラグを管理すると確かに効率は良いですが、コードが読みにくくなります。
慣れれば良いのかもしれませんが、やはり直感的にわかりにくいというのはどうしようもありません。
いくら効率が良いといっても、パソコン用ソフトとして作るならばわざわざビット演算でフラグ管理する必要性はあまりありません。
char型変数でフラグが1000個あっても1000バイトしか消費しませんし、char型配列等で管理しても処理時間はほとんど変わらないと言ってもいいでしょう。
関数の引数として渡す場合は変数一つを渡すだけで良いので、配列を渡す場合に比べて記述が簡単です。
Windows API(Windowsを操作するための関数群)には、関数の引数にビット演算が時々登場します。
hWnd = CreateWindow(
"ABC" , "ABC",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
100, 100, 500, 400,
NULL, NULL, hInstance ,NULL
);
CreateWindow
はウィンドウを表示するAPIです。
3行目のWS_OVERLAPPEDWINDOW | WS_VISIBLE
はまさにビット演算です。
ウィンドウをどのような状態で表示するかのフラグを、ビット演算を用いて渡しています。
といってもビット演算ということを意識しなくても、「こうやって渡す決まりだ」と覚えておくだけで不都合はありません。
なお、1ビット単位のデータを扱うには構造体のビットフィールドという方法もあります。
シフト演算
乗算、除算の代替
シフト演算は高速なので、掛け算/割り算の代わりに使用すると処理が高速になります。
number = 8;
// 1/2倍
number = number >> 1;
number >>= 1;
// 4倍
number = number << 2;
number <<= 2:
ただ、普通の計算時にシフト演算が混ざるとわかりにくくなりますし、速いといってもパソコン用ソフトでは実感できるレベルではないと思います。
何万回もループするような処理がある場合は、シフト演算を使った方がいいかもしれません。
コンパイラによっては通常の演算子による乗算/除算はシフト演算子による処理に置き換えられる場合もあります。
論理演算との組み合わせ
以下はunsigned char型のビットの並びを表示する関数です。
//unsigned char型のビット並びを表示
void PrintBitC(unsigned char c)
{
//二進数で「10000000」を取得
unsigned char bit = 1 << 7;
//型名からサイズを取得する場合は
//unsigned char bit = 1 << (sizeof(unsigned char) * 8 - 1);
while (bit)
{
if (c & bit)
putchar('1');
else
putchar('0');
bit >>= 1;
}
putchar('\n');
}
5行目のunsigned char bit = 1 << 7;
は、二進数で「10000000」を取得する処理です。
最上位ビット(左端)のみを「1」にした値は「データ型のサイズ × 8 - 1」で左シフトすると取得できます。
(1バイト = 8ビットのため)
- 00000001 (1) 1
- 00000010 (2) 1 << 1
- 00000100 (4) 1 << 2
- ...
- 10000000 (128) 1 << 7
次に、ループ文で上位ビットから順に0か1かを判別します。
例えば「10101010」という二進数をチェックすると以下のようになります。
- 10101010
- 10000000
- &
- 10000000
AND演算は、両方が1なら1、それ以外は0となりますから、結果は「10000000」となります。
条件判定では0以外は真となりますから、「10000000」は真となり、「1」が出力されます。
ループの最後では変数bit
をひとつ右シフトします。
(01000000)
そして、再度AND演算子で判定をします。
- 10101010
- 01000000
- &
- 00000000
今度は「00000000」、つまり0ですから、偽となり「0」が出力されます。
これを8回、つまり変数bit
が「00000000」になるまで繰り返せば、引数の値を二進数で表現した文字列が出力されることになります。
もちろん「00000000」(つまり0)は偽ですから、ループの終了条件判定でループは終了します。
この方法を使えば、flag変数をループで判定するなどの応用ができます。
今回は先頭から順に文字列を表示するために「10000000」を右シフトしながら判定していますが、フラグの判定なら「00000001」を左シフトしていく方法でも構いません。