開発効率化1:マクロと構造体

プログラムの開発を効率化するための技を修得してゆこう.

今回の学習内容は,ソースコードの可読性・保守性の向上のため...

要するに,ソースコードを書き易く・読み易くします.

教科書の該当範囲:第4.11節,第6章
参考書の該当範囲:第11章,第16章

マクロ

Step 1. 定数マクロの利用

定数マクロは,もう何度も書きました.

cmd.c

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

#define BUFLEN	256	// 定数マクロの定義

int main(void)
{
	char	cmd[BUFLEN];
	FILE	*fp = stdin;

	while (1) {
		printf("コマンド > ");
		fgets(cmd, BUFLEN, fp);	// 定数マクロの利用
		if (strcmp(cmd, "quit") == 0) break;
	}
	return (0);
}

コンパイルコマンドを実行すると, 前処理としてソースコード内のマクロ BUFLEN の部分が数値 256 へ置き換えられ, その置換後のソースコードがコンパイルされる.

コンパイルし実行しよう. "quit"の入力で終わるハズだが...終わらない...なぜだー??

Step 2. デバッグ機能の追加

間違いを探し出そう.

...
int main(void)
{
	...
	while (1) {
		printf("コマンド > ");
		fgets(cmd, BUFLEN, fp);
		printf("cmd:[%s]\n", cmd);	// デバッグ用コード
			// 変数(入力文字列)を表示してみる,i.e. printfデバッグ
		if (strcmp(cmd, "quit") == 0) break;
	}
	...
}

printfデバッグによって原因が判明... 配列cmd 内に改行文字'\n' も代入されている.

デバッグ用コードを削除して,再度コンパイルするのかい? 長いプログラムなら, 何度も書き直すのは面倒臭いぞ.

Step 3. フラグマクロによるデバッグ機能の切替

フラグマクロを利用し, デバッグ機能の on/off を簡単に切り替えられるようにします.

...

#define DEBUG		// フラグマクロの定義 ... デバッグモード on
// #undef DEBUG		// マクロ定義を解除 ... デバッグモード off
// または...
// #define DEBUG	// コード内では未定義とし,コンパイル時に切替
	/*
		$ cc -DDEBUG ...
		ソースコード内だけでなくコンパイル時にもマクロを定義できる.
		デバッグ作業の前後でコードの書き換えが不要になるよ.
	*/

#define BUFLEN	256

int main(void)
{
	...
	while (1) {
		printf("コマンド > ");
		fgets(cmd, BUFLEN, fp);
#ifdef DEBUG				// ここから...デバッグ用コード
		printf("cmd:[%s]\n", cmd);
#endif					// ...ここまで
		if (strcmp(cmd, "quit") == 0) break;
	}
	...
}

#ifdef DEBUG 〜 #endif の間のコードは, DEBUG が定義されている場合だけ,有効となる.

デバッグ以外の動作は変わりません.

Step 4. ソースコードの修正

コードの間違いを修正しよう.

...

// #define DEBUG
// #undef DEBUG

#define BUFLEN	256

int main(void)
{
	char	buf[BUFLEN];	// 行文字列('\n' あり)
	char	*cmd;		// コマンド文字列('\n' 除去)
	FILE	*fp = stdin;

	while (1) {
		printf("コマンド > ");
		fgets(buf, BUFLEN, fp);
			// fgets()では改行文字'\n'も配列に格納するのでした...
		cmd = strtok(buf, " \t\n");	// トークン分割
			// strtok()で'\n'等を除去
#ifdef DEBUG				// ここから...デバッグ用コード
		printf("cmd:[%s]\n", cmd);
#endif					// ...ここまで
		if (strcmp(cmd, "quit") == 0) break;
	}
	return (0);
}

問題は解決できた. しかし,strcmp()というかCの文字列は使いづらいぜ...

Step 5. 引数付きマクロによる言語仕様の変更

文字列処理を使い易く,C言語自体を改変してしまおう.

...

// 引数付きマクロ
#define StrEql(s1, s2)	(strcmp(s1, s2)	== 0)
	// 関数マクロ:文字列比較の条件式の定義
	/*
		この程度なら,関数として定義してもよいが,
		マクロの方が実行時間・メモリ使用量が少なく済む.
		(というか,増えずに済む.)
	*/

#define IfStr(s1, op, s2)	if (strcmp(s1, s2) op 0)
	// 制御構造マクロ:文字列比較専用のif文の定義

#define IfCmdIs(s)	if (strcmp(cmd, s) == 0)
	// 制御構造マクロ:cmd比較専用のif文の定義
	/*
		引数付きマクロでは,Cに新しい制御構造さえも追加できる.
		引数には,変数・定数だけでなく,演算子・関数なども利用できる.
		ただし,使いすぎると他人には理解不能なコードとなりがち.
		「過ぎたるは及ばざるが如し」
	*/

int main(void)
{
	...
	while (1) {
		...
//		if (strcmp(cmd, "quit") == 0) break;	// 使いづらいので...

	// マクロによる改変例
	// コンパイル時にマクロ↓ はどれでも元のコード↑ に展開される

		if (StrEql(cmd, "quit")) break;	// 意図を明確化...
//		IfStr(cmd, ==, "quit") break;	// 意図を更に明確化...
//		IfCmdIs("quit") break;		// 短く
	}
	return (0);
}

マクロの落とし穴

マクロは,関数とは異なり,実行時に計算してくれる訳ではなく, コンパイル時にソースコードの字面を置き換えるだけなので... 意外なコードに展開され,想定外の結果となる場合がある.

// 逆数を求める関数マクロ
#define Inv(x)  1.0/x		// イマイチな定義方法

/*
	使用例 ⇨ 展開結果 ⇨ 実行結果:
	Inv(2.0) ⇨ 1.0/2.0 ⇨ 0.5 ですが何か?

	Inv(2 + 3) ⇨ 1.0/2 + 3 ⇨ 3.5 ...えぇえー???
		 1/(2+3) = 1/5 = 0.2 じゃねーの?

	Inv(1/2) ⇨ 1.0/1/2 ⇨ 0.5 ... 逆数の逆数なので 2 に戻るハズだ

	1/Inv(2) ⇨ 1/1.0/2 ⇨ 0.5 ... 2 に戻ってください...
*/

// #define Inv(x)  (1.0/(x))	// 改善案

練習問題

前回作成の pdcl.c に制御構造マクロを導入せよ.


構造体

未完成です... とりあえず こちら (昨年度の授業内容)を参考に学習してください.

練習問題

前回作成の pendraw.c に構造体を導入せよ.

...

typedef struct {
	int	w, h;		// 元の変数 w, h
	char	*pixel;		// 元の変数 img
} Img;		// 画像の構造体

typedef struct {
	int	x, y;		// 元の変数 x, y
	int	state;		// 元の変数 pen
} Pen;		// ペンの構造体

...

void FillImg(Img *img, char c)
{
	int     x, y;

	for (y = 0; y < img->h; y++) {
		for (x = 0; x < img->w; x++) {
			img->pixel[img->w*y + x] = c;
		}
	}
}

...

void Game(...)
{
	...
	Pen	pen;	// 変数 pen,x,y を構造体1個に統合
	Img	img;	// 変数 img,w,h を構造体1個に統合
	int	w, h;	// 画面サイズでもあるので残しておく(img と重複)
	...
	getmaxyx(stdscr, h, w);
	...
	pen.state = 0;		// pen = 0
	pen.x = w/2;		// x = w/2
	pen.y = h/2;		// y = h/2
	...
	img.w = w;
	img.h = h;
	img.pixel = (char *)malloc(sizeof(char)*w*h);
	if (img.pixel == NULL) ...;
	FillImg(&img, ' '); 
LOOP:
	while (1) {
		...
		Draw(&img, &pen);
                erase();
                ShowImg(&img);
                ShowPen(&pen);    
		...
		Ctrl(&pen, &img, key)
		...
	}
	...
	free(img.pixel);
	...
}
...

構造体を関数呼出の引数・戻り値として使う場合, 常に,参照渡し(ポインタ)とするとよい. 値渡しでは,構造体のすべてのメンバ変数のコピーのために時間を浪費する. 参照渡しでは,構造体の先頭アドレス1個だけのコピーで済むので, 処理時間もメモリ使用量も少ない.


(c) 2018, yanagawa@kushiro-ct.ac.jp