見出し画像

【C言語プログラミング6】ルフィを描く!解説3

今回で解説はラストになります。ビット演算、DecompressLuffy関数の詳細、PrintLuffy関数の詳細について説明します。

ビット演算

ビット演算とはデータをビット単位で扱って演算することを言います。例えば16進数で0x01という値があったとします。0x01を2進数で表すと、0000 0001となります。ビット演算では、あるデータのある特定のビットの状態が0なのか1なのを調べたり、ある特定のビットの状態だけを0から1に変えたりする場合に使用します。

&演算(AND演算)

&演算(AND演算)はあるビットとあるビットの論理積を取ります。具体例を示します。2つの16進数の値(0x01と0x81)の&演算をすると以下のようになります。2つの値の各ビットを掛け算した結果が演算結果になります。

画像4

最下位ビット(右端)は1×1=1、2~7ビット目は0×0=0、最上位ビット(左端)は0×1=0となります。AND演算をC言語で書いた場合は以下のようになります。

unsigned char a = 0x01;
unsigned char b = 0x81;
unsigned char c;

c = a & b;              /* cには0x01が入ります */

|演算(OR演算)

|演算(OR演算)はあるビットとあるビットの論理和を取ります。具体例を示します。2つの16進数の値は0x01と0x80とします。|演算をすると以下のようになります。2つの値の各ビットのどちらか一方が1ならば演算結果は1となります。

画像4

C言語でOR演算を書いた場合は以下のようになります。

unsigned char a = 0x01;
unsigned char b = 0x80;
unsigned char c;

c = a | b;            /* cには0x81が入ります */

ビットシフト

ビットシフトはビット列全体を左右に何れかにシフトさせる演算になります。具体例を示します。16進数のデータ0x0Fがあるとします。これを左にビットシフトさせると以下の図のようになります。左にシフトした際に右側に空いた隙間には0が埋まります。この例の場合、左へ8回ビットシフトすると全ビットが0になります。

ビットシフト

ビットシフトをC言語で書くと以下のようになります。

unsigned char a = 0x0F;
unsigned char b;

b = a << 1;        /* bには0x1Eが入ります */

右にビットシフトする時も考え方は同じなのですが、変数の型が-符号を扱うか否かでシフト後に空いた隙間を埋める値の0/1が変わります。符号ありの場合は1で埋められ、符号なしの場合は0で埋められます。具体的にコードで書くと以下の通りです。

char a = 0x01;     /* 符号あり */
char b;

b = a >> 1;        /* bには0x80が入ります */
unsigned char a = 0x01;    /* 符号なし */
unsigned char b;

b = a >> 1;                /* bには0x00が入ります */

ここまでの知識でルフィを描くことができます。

ルフィの画像データを"☐"と"■"に変換する

いよいよ、これまで説明した知識を使ってルフィの16進数画像データを"☐"と"■"のデータに変換してみましょう。

プログラムを実行した時にコンソールに表示されるルフィの一番上の行の左から32個分の四角に注目すると、”☐☐☐☐☐☐☐☐☐☐☐☐☐☐☐☐☐☐☐☐☐☐☐☐■☐☐☐☐☐■☐”こうなっていると思います。☐=0、■=1とすると、0000 0000 0000 0000 0000 0000 1000 0010となりますよね。これを16進数に変換すると、0x00、0x00、0x00、0x82になります。

では、ルフィ描画プログラムのソースコードに戻って、ルフィの画像データであるunsigned char gucImageData[1250]の先頭から4バイト分のデータを見てみましょう。0x00、0x00、0x00、0x41・・ん?0x41?0x082じゃないの?先程、☐と■を0と1に置き換えた数値を16進数に変換すると、0x00、0x00、0x00、0x82でしたね。実はこれは諸事情により、ビットの並びが逆転しているのです。つまり変換する時には以下のように逆転しなければなりません。この処理をDecompressLuffyData関数の中で実行しています。

ビットオーダー

DecompressLuffyData関数の中身

void DecompressLuffyData(void)
{
	int Hcount;
	int Wcount;
	int bytePos = 0;
	int bitsift = 0;

	for (Hcount = 0; Hcount < IMAGE_HEIGHT; Hcount++) {
		for (Wcount = 0; Wcount < IMAGE_WIDTH; Wcount++) {
			gucImage[Hcount][Wcount] = (gucImageData[bytePos] >> bitsift) & 0x01;

			if (bitsift == 7) {
				bitsift = 0;
				bytePos++;
			}
			else {
				bitsift++;
			}
		}
	}
}

この関数は2重ループを使っています。ループの中でループするのです。非常にややこしく感じると思いますが、落ち着いて考えると難しくありません。掛け算の九九と同じ考え方です。九九も表にすると9×9のマス目になりますよね。まず1の段で9回掛け算する、次の2の段で9回掛け算する・・・つまり、1段最後まで処理をやって次の段に移るという流れです。

九九の場合は9×9で81個のマスになりますが。今回は100×100で10000個のマスになります。数が大きいですが、大きいだけで何も変わりません。やりたいことは、一番左上のマス(gucImage[0][0])は”☐”ですか?”■”ですか?、その右隣は(gucImage[0][1])は”☐”ですか?”■ですか?”を一個ずつ決めることです。

”☐”=0、”■”=1として、その情報を二次元配列のunsigned char gucImage[0][0]~[99][99]に順番に入れて行きます。当然繰り返しになるのでfor文を使います。

gucImage[0][0] ⇒ 一番上の行の左端のこと
gucImage[0][99] ⇒ 一番上の行の右端のこと
gucImage[99][0] ⇒ 一番下の行の左端のこと
gucImage[99][99] ⇒ 一番下の行の右端のこと

forループ処理

"☐"か"■"を表す0または1のデータを10000個用意しないといけないと言うことは、10000回のループをしないといけません。今回はそれを2重ループで実現しています。外のループでは縦のループ(100回)を、内のループでは横のループ(100回)をします。処理の流れとしては、まず先頭行の左端から右端に向かって0または1の値を入れて行きます。先頭行が[99]まで埋まったら、2行目、3行目と100行目まで繰り返します。

gucImage[0][0] ⇒ gucImage[0][1]・・・gucImage[0][99] ⇒ gucImage[1][0] ⇒ gucImage[1][1]・・・gucImage[1][99] ・・・gucImage[99][0] ⇒ gucImage[99][1]・・・gucImage[99][99]

gucImage[0][0]に0/1のどちらかの値を入れる、gucImage[0][1]に0/1のどちらかの値を入れる、という順番に処理されて行きます。

ビット演算処理

gucImage[Hcount][Wcount] = (gucImageData[bytePos] >> bitsift) & 0x01;

この処理が今回のプログラムの中で一番複雑な処理です。このコードをそのまま日本語にすると、二次元配列のある場所に、一次元配列のある場所のデータをbitsift分右にシフトし、そのデータと0x01のAND演算した結果を格納する。意味分かりませんね。具体的な値を使って動きを確認します。

Hcount = 0、Wcount = 0、bytePos = 0、bitsift = 0とします。これはループ処理が始まった最初の状態です。

gucImageData[bytePos]は具体的にはgucImageData[0]のことですので、0x00です。これをbitsift分右にシフトします。bitsift = 0なので、0x00を右に0ビットシフトするということです。つまり、シフトしません。そしてその値と0x01をAND演算します。つまり、0x00 AND 0x01という演算になります。この演算結果は0x00となります。そしてこの演算結果をgucImageData[0][0]に入れるので、gucImageData[0][0] = 0x00;となります。ということは一番上の行の一番左端は0ということになります。先頭行の一番左は”☐”ということです。

一つのマス目の0/1の値が決まれば、次のマス目の0/1を決める為に、bitsift数とbytePosを更新します。gucImageDataの1バイトのビットチェックが終われば、次のバイトに進んでいます。bitsiftが7まで来たと言うことは、1バイト分のビットチェックが終わったことになりますので、bitsift = 0、bytePosを1プラスしています。

if (bitsift == 7) {
	bitsift = 0;
	bytePos++;
}
else {
	bitsift++;
}

これを見てパッと頭の中で、データがどんな動きをして、結果としてどうなるのかが分かる人は、Cを喋れる人でしょうね。恐らく難し!って思うでしょう。紙か何かに変数の値を書き出して、今この変数の中身がこうなって、この演算結果がこうなってるから、結果はこうだ!と順に追いかけないと迷子になると思います。最初は以下のような表を作って、変数の中身を頭の外に書き出して確認する必要がありますね。慣れれば脳内で全部できるようになります。

ロジック確認表

何にせよやっていることは、gucImage[0][0] = ○○; ・・ gucImage[99][99] = ○○; と言うことです。ちなみに、DecompressLuffyData関数の処理が終わった後、gucImage[0][0]~[99][99]までの内容をprintfするとこうなります。0/1ですが既にルフィに見えますよね。先頭行の最初から25番目と31番目が1になっていますね。上の表と一致することが分かると思います。

画像6

PrintLuffy関数

この関数はDecompressLuffyData関数に比べると簡単でしょう。DecompressLuffyData関数が作ってくれた、0/1情報を元に0だったら”☐”、1だったら”■”をprintf関数を使ってコンソールに出力しているだけです。

void PrintLuffy(void)
{
	int Hcount;
	int Wcount;

	for (Hcount = 0; Hcount < IMAGE_HEIGHT; Hcount++) {
		for (Wcount = 0; Wcount < IMAGE_WIDTH; Wcount++) {
			Sleep(1);

			if (gucImage[Hcount][Wcount] == 0) {
				printf("■");
			}
			else {
				printf("□");
			}
		}
		printf("\n");
	}
}

PrintLuffy関数を以下のように変更すると、先程の0/1ルフィになります。

void PrintLuffy(void)
{
	int Hcount;
	int Wcount;

	for (Hcount = 0; Hcount < IMAGE_HEIGHT; Hcount++) {
		for (Wcount = 0; Wcount < IMAGE_WIDTH; Wcount++) {
			Sleep(1);

			printf("%d", gucImage[Hcount][Wcount]);
		}
		printf("\n");
	}
}

Sleep関数

PrintLuffy関数内にSleep(1);という記述がありますが、これはSleep関数をコールしています。これもprintf関数と同様に誰かが用意してくれている関数で、#include <windows.h>により取り込んでいます。Sleep関数を使うと、処理を少し待たせることができます。今回はルフィが少しずつ描き出される風に見せたかったので、Sleep関数を使っています。この一行を削除すると、一瞬でルフィが描かれるようになります。

ローカル変数とグローバル変数

変数についての補足になります。DecompressLuffyData関数やPrintLuffy関数の中に、int HcountやWcountなどの変数を宣言しています。このように関数の内部で宣言する変数のことをローカル変数と言います。

DecompressLuffyData関数内にはint bitshiftやbyteposの変数が宣言されていますが、PrintLuffy関数内にはこの2つの変数は宣言されていません。もし、PrintLuffy関数内で、bytepos = 100;なんて記述をすると、コンパイルエラーになります。コンパイラーは「この変数なんすか?」って怒ってきます。つまり、ローカル変数はその変数を宣言した関数内でしか使えません。

逆にグローバル変数はどの関数からでも使うことができます。その証拠にgucImageDataやgcuImageはDecompressLuffyData関数やPrintLuffy関数内では宣言していませんね。これらは関数の外側で宣言されているのです。

家のベランダから月は見えますが、他人の家のテレビは見えませんよね。グローバル変数は月、ローカル変数は他人の家のテレビだと思って下さい。

define定義の意味

forループ処理の継続条件にIMAGE_HEIGHT(100のこと)、IMAGE_WIDTH(100のこと)というdefine値を使っていますが、直接100って書いちゃダメなのか?と思いますね。絶対にダメではありませんが、defineで定義しておいた方が後々楽できます。

もし、絵の縦横の幅が50になった時に、100という値を直接書いていると、色んな箇所を50に変更しないといけなくなります。これがdefine定義にしていると、#define IMAGE_HEIGHT 100としているのを#define IMAGE_HEIGHT 50と一箇所変更するだけで作業完了となります。

まとめ

これで一応一通りの説明は終わりです。後半難しくなったかも知れませんが、じっくり時間をかけて処理を追いかければ理解はできると思います。今回はルフィでしたが、このプログラムはルフィを描画する専用のプログラムではなく、gucImageData[1250]に格納するデータがゾロであれば、ゾロが描画され、サンジであればサンジが描画されます。

C言語にはこの他にも構造体やらポインタやら色々あるので、実践ではこう使いますと言う観点で紹介して行こうかと思います。それでは、お疲れ様でした。

この記事が気に入ったらサポートをしてみませんか?