プログラムの開発を効率化するための技を修得してゆこう.
今回の学習内容は,ソースコードの可読性・保守性の向上のため...
マクロと構造体をうまく利用すれば, ソースコードの記述効率(作成速度)を高められるだけでなく, プログラムの実行効率(処理速度)を高められる場合もある.
マクロ定義の書式:
#define マクロ名 数値など
マクロ定義・利用の(かなり極端な)例:
// マクロの定義 #define TYPE char #define SIZE 256 #define VAR buf #define INPUT scanf("%s", buf) #define OUTPUT printf("%s\n", buf) // マクロの利用 TYPE VAR[SIZE]; 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-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 ...
動作は以前と変わりないハズ.
何がどうなったのか? 構造体を導入し,関数呼出における引数の個数を削減し, ソースコードが短くなった. つまり,記述効率が向上した.
次に,実行効率についても改善してみよう. ここまでの段階では, ユーザ関数 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個だけのコピーで済み, 呼出元と呼出先の関数間でメモリ領域を共有するので, 処理時間もメモリ使用量も少ない.