プログラムの開発を効率化するための技を修得してゆこう.
今回の学習内容は,ソースコードの可読性・保守性の向上のため...
マクロと構造体をうまく利用すれば, ソースコードの記述効率(作成速度)を高められるだけでなく, プログラムの実行効率(処理速度)を高められる場合もある.
マクロ定義の書式:
#define マクロ名 数値など
マクロ定義・利用の(かなり極端な)例:
// マクロの定義
#define INIT char buf[256]
#define INPUT scanf("%s", buf)
#define OUTPUT printf("%s\n", buf)
// マクロの利用
INIT;
INPUT;
OUTPUT;
マクロ展開の例:(上のコードが次のように置き換えられる.)
char buf[256];
scanf("%s", buf);
printf("%s\n", buf);
マクロ定義の書式:
#define マクロ名(仮引数, ...) 数式など
マクロ定義・利用の(ちょっと怪しい)例:
#define SQR(x) x*x z = SQR(y);
マクロ展開の例:
z = y*y;
フラグ変数(値を 0 or 1 とする変数)と if 文のようなマクロ.
マクロ定義の書式:
#define マクロ名 // マクロ名を定義(変数 = 1; みたいな)
#undef マクロ名 // マクロ名を解除(変数 = 0; のような)
#ifdef マクロ名 // マクロ名が定義済みの場合だけ...(if (変数 != 0) {)
... // ...この部分のコードをコンパイル
#endif // ifdef の終了(})
定数マクロについては,すでに前期から何度も使ってきましたが,再確認します.
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 なのですが,このプログラムには他に間違いがあります.
間違いを探し出そう.
...
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' も代入されている.
デバッグ用コードを削除して,再度コンパイルするのかい? もし,多数のバグを含む長〜いソースコードなら, 何箇所も何度も書き直すのは面倒臭いぞ.
フラグマクロを利用し, デバッグ機能の 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 が定義されている場合だけ,有効となる.
デバッグ以外の動作は変わりません.
コードの間違いを修正しよう.
...// #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の文字列は,使いづらいぜ...
引数付きマクロを利用して,文字列処理を使い易く, 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 の制御構造マクロを導入せよ.
複数個の変数をグループ化し,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個のコピー)とするのが効率的なのでオススメ.
以前作成した ttt-2.c(ttt.c の ver.2,課題で作成したハズの動的配列版)に 構造体を導入してみよう.
以前作成したソースファイルを本日のディレクトリにコピーしてから, 編集を開始しよう. (ファイル名については,各自の状況に合わせて読み替えるんですよ.)
$ cp ~/c-1027/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 ...
動作は以前と変わりないハズ.
何がどうなったのか? 構造体を導入し,関数呼出における引数の個数を削減し, ソースコードが短くなった. つまり,記述効率が向上した.
次に,実行効率についても改善してみよう. ここまでの段階では, ユーザ関数 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 ...
実行結果を確認しよう.
理論的には,実行効率も改善しているハズ. (測定による実証は必要だが,今回は割愛.)
ここまでの段階では,関数呼出の際にだけ,構造体のポインタを利用したが, メイン関数内ではまだ,構造体の実体も併用している. 構造体の実体/ポインタの使い分けでは, コードの記述方法が微妙に異なり,混乱しがちではないだろうか?
ソースコード内のすべての構造体をポインタに統一してみよう.
$ 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;
...
説明済みですが,念のため... 構造体を関数呼出の引数・戻り値として使う場合, 常に,参照渡し(ポインタ引数)とするとよい.
値渡しでは,構造体のすべてのメンバ変数をコピーするため, メモリ領域を2倍使うだけでなく, コピー処理に時間もかかる.
参照渡しでは, 構造体の先頭アドレス1個だけのコピーで済み, 呼出元と呼出先の関数間でメモリ領域を共有するので, 処理時間もメモリ使用量も少ない.