開発の効率化1(マクロと構造体)

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

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

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

マクロと構造体をうまく利用すれば, ソースコードの記述効率(作成速度)を高められるだけでなく, プログラムの実行効率(処理速度)を高められる場合もある.

教科書の該当範囲:第8.1節,第11.1節

マクロ

基礎知識
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 へ置き換えられ, その置換後のソースコードがコンパイルされることになる.

プリプロセッサだけを実行し,確かめてみよう.

$ cpp cmd.c | less
...
...		// ヘッダファイルの内容が延々と続き...
...

int main(void)
{
	char cmd[256];			// コードが置換された
	...
	while (1) {
		printf("コマンド > ");
		fgets(cmd, 256, fp);	// コードが置換された
		if (strcmp(cmd, "quit") == 0) break;
	}
	...
}

コンパイルし,実行しよう.

$ cc cmd.c -o cmd
$ ./cmd
quit
...
[Ctrl]+[C]

あれれ,"quit"の入力で終わるハズだが...終わらない...なぜだー?? はい,定数マクロは OK なのですが,このプログラムには他に間違いがあります.

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;
	}
	...
}
より適切には,標準エラー出力 fprintf(stderr, ...) を使うべき. 標準出力だと,バッファリングによって表示されない場合もあるので.

実行結果を観察しよう. 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	// cc -DDEBUG を使うなら不要
// #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)
	/*
		この程度なら,関数として定義してもよいが,
		マクロの方が実行時間・メモリ使用量が少なく済む.
		(というか,増えずに済む.)
	*/

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

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

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

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

		if StrEql(cmd, "quit") break;	// 意図を明確化
//		IfStr(cmd, ==, "quit") break;	// 意図を更に明確化
			// よくある間違い「if (cmd == "quit") ...」の代用
//		IfCmdIs("quit") break;		// コードを短縮
			// できると便利な「switch (cmd) { case "quit": ... }」の代用
	}
	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 に対して, Step 5 の制御構造マクロを導入せよ.

大量に書いていた if (strcmp(...) == 0) ... を短縮しよう.
「二度手間かよ」とか「先に説明してよ」って? 先に不便を感じるほど,学習の価値も感じられるのでは?

構造体

基礎知識

複数個の変数をグループ化し,1個の構造体変数として取り扱える. グループ内のそれぞれのメンバ変数にも個別にアクセスできる. また,メンバ変数の型はそれぞれ異なってよい.

変数のグループ化の方法として,配列もありましたね. ただし,配列の場合,すべての要素変数の型が同じでなければならないし, すべての要素を一体化して取り扱うこともできない.

構造体の基本的な利用方法について, 次のソースコードから読み解こう.

complex.c

#include <stdio.h>

// 複素数の構造体型の定義
typedef struct {
	double	re, im;		// 実数部,虚数部のメンバ変数
	char	*text;		// 説明文のメンバ変数
} Complex;		// 構造体の型名

// 複素数を表示する関数
void PrintC(char *name, Complex *z)	// 構造体の仮引数(普通の変数と同様)
{
	printf("%s = %f + %f i ... %s\n", name, z->re, z->im, z->text);
		// メンバ変数に分解して表示
		// 「->」は構造体のポインタに対するメンバアクセス演算子
}

// 複素数を初期化する関数
Complex InitC(double re, double im, char *text)
{
	Complex	c;
	c.re = re;
	c.im = im;
	c.text = text;
	return (c);
}

int main(void)
{
	Complex	a;	// 構造体変数の宣言
	Complex	b = {0.0, 1.0, "虚数単位"};	// 構造体変数の宣言と初期化
	Complex	*p;	// 構造体へのポインタの宣言

//	b = 2*b;	// 構造体の計算...NG
		// (独自に計算関数を定義し,b = MulC(b, 2); などとせよ)
	PrintC("b", &b);	// 構造体の実引数(普通の変数と同様)

	a = b;		// 構造体同士の一括代入(全メンバのコピー)
//	if (a == b) printf("Bingo!\n");	// 構造体同士の比較...NG(独自に関数等を定義せよ)
	PrintC("a", &a);

//	b = {0.0, 0.0, "ゼロ"};		// 宣言以外での初期化...NG
	b.re = 0.0;		// メンバ変数への代入
	b.im = 0.0;		//     〃
	b.text = "ゼロ";		//     〃
		// 「b.re」等の「.」は構造体の実体に対するメンバアクセス演算子
	// ↑ 個別代入は面倒... ならば,独自に初期化関数を定義せよ↓.
//	b = InitC(0.0, 0.0, "ゼロ");
		// 構造体を作り,初期値(引数)を個別代入し,その構造体を返す関数ね.
	PrintC("b", &b);

	p = &b;		// 構造体ポインタへの構造体アドレスの代入
	PrintC("b", p);

	a = *p;		// 要するに a = b と同
	PrintC("a", &a);

	return (0);
}

なお,構造体を関数の引数として利用する場合には, 構造体の実体(全メンバのコピー)としても実行できるが, ポインタ(アドレス1個のコピー)とするのが効率的なのでオススメ.

実行速度的にもメモリ使用量的にもポインタ引数の方が効率的.
Step 1. 構造体の利用(初級)

以前作成した ttt-2.cttt.c の ver.2,課題で作成したハズの動的配列版)に 構造体を導入してみよう.

以前作成したソースファイルを本日のディレクトリにコピーしてから, 編集を開始しよう. (ファイル名については,各自の状況に合わせて読み替えるんですよ.)

$ cp  ~/c-1030/ttt-2.c  ./ttt-3.c	# ディレクトリ名とファイル名は人それぞれ,適切に読み替え
$ vim  ttt-3.c

まずは,本体の部分を改造...

/* Tic-Tac-Toe 超簡易版 */
/* ver.2 動的配列版 */
// ver.3 構造体版(初級:記述効率の改善)

#include ...

// ゲーム盤の構造体型の定義
typedef struct {
	int	size;	// ゲーム盤のサイズ(旧変数名:n)
	int	*cell;	// マスの動的配列へのポインタ(旧変数名:bd)
} Board;
...

int main(void)
{
//	int	*bd;	/* ゲーム盤の動的配列へのポインタ */
	Board	bd;	// ゲーム盤構造体の変数(実体)
	int	n;	/* ゲーム盤のサイズ */
	...

	printf("ゲーム盤のサイズ > ");
	scanf("%d", &n);

//	bd = (int *)malloc(sizeof(int)*n*n);
//	if (bd.cell == NULL) return (1);
	bd.size = n;					// ゲーム盤のサイズと...
	bd.cell = (int *)malloc(sizeof(int)*n*n);	// ...動的配列とを構造体に統合したよ
	if (bd.cell == NULL) return (1);

//	Clear(bd, n);	// ゲーム盤に関する変数2個 bd と n は...
	Clear(bd);	// ...構造体1個 bd に統合されてるよ.他の関数についても同様に...

	player = 1;
	while (1) {
		Draw(bd);				// 同上

		while (1) {
			...
			if (Get(bd, y, x) == 0) break;	// 同上
			...
		}
		Set(bd, y, x, player);			// 同上
		...
	}
END:
	printf("\n終了\n");
//	free(bd);		// 動的配列 bd は構造体に組み込まれ...
	free(bd.cell);		// ... bd.cell になったよ

	return (0);
}

まだコンパイルはできない. メイン関数などの変更に合わせて,各ユーザ関数の定義も変更...

...
// int Get(int *bd, int n, int y, int x)
int Get(Board bd, int y, int x)
{
	...
	if (x >= bd.size) return (-1);
	...
	if (y >= bd.size) return (-1);

	return (bd.cell[y*bd.size + x]);
}

void Set(Board bd, int y, int x, int v)
{
	...		// 同様に...
}

void Clear(Board bd)
{
	...		// 同様に...
}

void Draw(Board bd)
{
	...		// 同様に...
}
...

とりあえず,この段階は完了. コンパイルし,実行してみよう.

$ cc  ttt-3.c  -o  ttt-3
$ ./ttt-3
...

動作は以前と変わりないハズ.

何がどうなったのか? 構造体を導入し,関数呼出における引数の個数を削減し, ソースコードが短くなった. つまり,記述効率が向上した.

もちろん,構造体定義などの手間は増えているが, それは一度だけなので,影響は少ない.
一方,変数は何度も利用するので, 変数の個数を減らせば,効果は大きい.
Step 2. 構造体の利用(中級)

次に,実行効率についても改善してみよう. ここまでの段階では, ユーザ関数 Draw()Get()Set(),等の呼び出しの際, 構造体変数 bd の実体をそのまま引数として利用していた. このとき,コード的には構造体変数1個だけを受け渡しているように見えるが, 実行時には構造体内の全メンバ変数がコピーされている. コピーされるデータ量に比例してメモリ容量も実行時間も無駄になる.

というわけで,関数呼出時にコピーされるデータ量を削減してみよう. 引数として構造体のポインタを利用すれば, 構造体のメンバ数には関係なく, アドレス1個分だけの受け渡しで済む.

$ cp  ttt-3.c  ./ttt-4.c
$ vim  ttt-4.c
...
// ver.4 構造体版(中級:実行効率も改善)
...

// int Get(Board bd, int y, int x)	// 仮引数を構造体の実体から...
int Get(Board *bd, int y, int x)	// ...ポインタに変更
{
	...
	if (x >= bd->size) return (-1);
	...
	if (y >= bd->size) return (-1);

	return (bd->cell[y*bd->size + x]);
}

void Set(Board *bd, int y, int x, int v)
{
	...		// 同様に...
}

void Clear(Board *bd)
{
	...		// 同様に...
}

void Draw(Board *bd)
{
	...		// 同様に...
}

int main(void)
{
	...

//	Clear(bd);		// 実引数を構造体の実体から...
	Clear(&bd);		// ...ポインタに変更

	player = 1;
	while (1) {
		Draw(&bd);				// 同上

		while (1) {
			...
			if (Get(&bd, y, x) == 0) break;	// 同上
			...
		}
		Set(&bd, y, x, player);			// 同上
		...
	}
	...
}
$ cc  ttt-4.c  -o  ttt-4
$ ./ttt-4
...

実行結果を確認しよう.

理論的には,実行効率も改善しているハズ. (測定による実証は必要だが,今回は割愛.)

Step 3. 構造体の利用(上級)

ここまでの段階では,関数呼出の際にだけ,構造体のポインタを利用したが, メイン関数内ではまだ,構造体の実体も併用している. 構造体の実体/ポインタの使い分けでは, コードの記述方法が微妙に異なり,混乱しがちではないだろうか?

メンバ変数の指定は「.」なのか 「->」なのか,どっちよ?

ソースコード内のすべての構造体をポインタに統一してみよう.

$ cp  ttt-4.c  ttt-5.c
$ vim  ttt-5.c
...
// ver.5 構造体版(上級:記述効率のさらなる改善)
...

// ver.5 では構造体をすべてポインタに統一するよ.そのための関数も追加ね.
// ゲーム盤を始末する関数(メモリ領域を開放)
void Free(Board *bd)
{
	if (bd == NULL) return;
	free(bd->cell);
	free(bd);
}

// ゲーム盤を準備する関数(メモリ領域を確保し,初期値を設定)
Board *New(int n)
{
	Board   *bd;

	// 構造体 Board 自身のメモリ領域の確保
	// ...全メンバ変数(ポインタ cell と変数 size)の格納用ね
	bd = (Board *)malloc(sizeof(Board));
	if (bd == NULL) return (NULL);

	// メンバ cell 配列のメモリ領域の確保
	// ... マス配列 int cell[n*n] の実体の格納用ね
	bd->cell = (int *)malloc(sizeof(int)*n*n);
	if (bd->cell == NULL) {	// bd->cell の確保に失敗した場合...
		Free(bd);	// bd 自身は確保済みなので,忘れずに解放!!
		return (NULL);
	}

	// サイズの設定
	bd->size = n;

	return (bd);
}

int main(void)
{
//	Board	bd;	// 構造体変数の実体だったものを...
	Board	*bd;	// ...構造体へのポインタに変更
	...

//	bd.size = ...;	// 関数化したよ
//	bd.cell = ...;
//	if (bd.cell == NULL) ...;
	bd = New(n);		// 構造体を準備(メモリ確保,初期値設定)
	if (bd == NULL) return (1);
	...

//	Clear(&bd);	// 実体だったのでポインタ化してたけど...
	Clear(bd);	// ...今はそのまま(& なし)でポインタだよ

	player = 1;
	while (1) {
		Draw(bd);		// 同上

		while (1) {
			...
			if (Get(bd, y, x) == 0) break;	// 同上
			...
		}
		Set(bd, y, x, player);	// 同上
		...
	}
	...
END:
	printf(...);
//	free(bd.cell);	// 関数化したよ
	Free(bd);		// 構造体を始末(メモリ開放)

	return (0);
}

記述効率...本当に高まったのか? コード量が増えたし,一部の内容は元に戻ったようだが?

プログラム全体的には...確かに, 今回のような小規模なプログラムでは実感できません. より複雑で大規模なプログラムで初めて効果が現れます.

また,小規模なプログラムでも, メイン関数にだけ注目すれば, 短く,見通し良くなったハズ.

練習問題

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

typedef struct {
	int	x, y;		// 元の変数 x, y
	int	state;		// 元の変数 pen
} Pen;		// ペンの構造体
...
	int	pen;
	int	x, y;
	Pen	pen;
...
個別の変数 penxy に関するコードの全てを 構造体変数 Pen pen に置き換えるんですよ.

説明済みですが,念のため... 構造体を関数呼出の引数・戻り値として使う場合, 常に,参照渡し(ポインタ引数)とするとよい.

値渡しでは,構造体のすべてのメンバ変数をコピーするため, メモリ領域を2倍使うだけでなく, コピー処理に時間もかかる.

参照渡しでは, 構造体の先頭アドレス1個だけのコピーで済み, 呼出元と呼出先の関数間でメモリ領域を共有するので, 処理時間もメモリ使用量も少ない.