昨日の時点で,ソースコードは,以下の通りです.
#include <stdio.h> #include <stdlib.h> #include <string.h> #define PLAYER_MAX 20 typedef struct { char player[PLAYER_MAX + 1]; int table[PLAYER_MAX][PLAYER_MAX]; int player_number; } rrtable; rrtable *initialize(char *player) { rrtable *tab; if (strlen(player) < 2) { printf("Too few players.\n"); exit(1); } if (strlen(player) > PLAYER_MAX) { printf("Too many players.\n"); exit(1); } if ((tab = (rrtable *)malloc(sizeof(rrtable))) == NULL) { printf("Malloc error.\n"); exit(1); } strcpy(tab->player, player); tab->player_number = strlen(player); memset((void *)tab->table, 0, PLAYER_MAX * PLAYER_MAX); return tab; } int main(void) { return 0; }
エラーはないようにしていますが,mainが実質からっぽなので,実行しても何もなりません.今日は,構造体を初期化して,その中身を出力するところを,取りかかっていきます.
昨日も述べましたが,「memset((void *)tab->table, 0, PLAYER_MAX * PLAYER_MAX);」は適切ではありません.2次元配列の表の初期値は,
- 同一対戦者のところは「-」に相当する情報
- それ以外は「 」に相当する情報
としなければなりません.2次元配列にどんな値を格納すべきか,突っ込んで考えます.
表の情報としては,charの2次元配列か,文字列の配列とするのが直感的ですが,そのプログラミング方法は別の機会に譲ることにします.ここでは,2次元配列の各要素には,出力する文字そのものではなく,「未対戦」「勝ち」「負け」「同一なので対戦しない」に対応づけられた整数値のひとつを格納することにします.その値を,出力時に文字に変換します.
「未対戦」「勝ち」「負け」「同一なので対戦しない」という異なる情報を表現するには…列挙型を使うのが自然です.
enum result { /* 勝敗情報を表す列挙型 */ RSLT_NOTPLAYED, /* 未対戦 */ RSLT_WIN, /* 勝 */ RSLT_LOSE, /* 負 */ RSLT_NA /* (同一プレイヤー同士なので)対戦しない */ };
列挙型の値の意味,そして型の役割については,今のうちにコメントとして書いておきます.
定数名の最初の「RSLT」は,resultから母音字を取ることで得られる省略形*1をつけ,「_」で結ぶことで,列挙定数の所在が分かりやすくなります.列挙型がひとつなら,ここまでする必要はありませんが,複数の列挙型を定義する際には,必ずこうしなければなりません.
「RSLT_」の後ろについて,WINとLOSEはいいとして,NAは,not applicableの略です.その一方で,NOTPLAYEDは略語にしていません.あまりいい名前の付け方ではありませんね.
ともあれ,これを使って,initialize関数のところを変更しましょう.
memset((void *)tab->table, RSLT_NOTPLAYED, PLAYER_MAX * PLAYER_MAX); for (i = 0; i < tab->player_number; i++) { tab->table[i][i] = RSLT_NA; }
i番目のプレイヤーとi番目のプレイヤーは,対戦しないので,初期化の際に,RSLT_NAを代入しておきます.紙に総当たり表を書くときの,斜め線を引く操作に対応します.そうそう,変数iを関数内で宣言しておくのを忘れないようにしないと.
あとはプログラム完成まで邁進…といきたいのですが,すべての関数を定義してから,コンパイルしてデバッグすると,バグが見つけにくいことがあります.そこで,初期化関数を書いたら,次に,その構造体の中身を見るための関数を定義し,mainから呼び出して,ここまでで動くようにしましょう.
/* 表を出力する */ void print_table(rrtable *tab) { int i, j; /* 最初の行 */ printf(" %s\n", tab->player); /* 2行目以降: プレイヤー名と成績 */ for (i = 0; i < tab->player_number; i++) { putchar(tab->player[i]); for (j = 0; j < tab->player_number; j++) { switch (tab->table[i][j]) { case RSLT_NOTPLAYED: putchar(' '); break; case RSLT_WIN: putchar('o'); break; case RSLT_LOSE: putchar('x'); break; case RSLT_NA: putchar('-'); break; default: putchar('?'); } } putchar('\n'); } }
(2)のときに決めた仕様の通りに,1行目と,2行目以降に分けて出力します.「tab->player[i]」は,i番目のプレイヤーの名前を表す,char型の値です.「tab->table[i][j]」は,i番目のプレイヤーが,j番目のプレイヤーに勝ったか負けたかを示す,整数値です.iもjもここでは,0が最小値(先頭)で,最大値(末尾)は,tab->player_number - 1です.
そして,main関数に情報を追加し,rrtable構造体を初期化して,試合開始前の総当たり表を出力するようにしましょう.
rrtable *tab;
tab = initialize("WNM");
print_table(tab);
コンパイルして,エラーがでなくなるまで取り除き,実行して
WNM W- N - M -
と出てきたら,まずは成功です.
もう一押し,しましょう.tab->playerに格納されることになる文字列を,標準入力から受け取る処理です.上の「tab = initialize("WNM");」を取り除き,代わりに以下のコードをつけます.
if (fgets(line, LINE_BYTE_MAX + 1, stdin) == NULL) { printf("No input.\n"); return 1; } if (strlen(line) > 1 && line[strlen(line) - 1] == '\n') { line[strlen(line) - 1] = '\0'; } tab = initialize(line);
前後しますが,(mainの中で)変数tabを宣言する直前に,配列変数lineも宣言しておきましょう.
#define LINE_BYTE_MAX 25 char line[LINE_BYTE_MAX + 1];
LINE_BYTE_MAXは,1行の文字列の最大バイト数です.一気に書き上げたいときに,ファイルの先頭に移動して#defineを書いて,また戻ってそれを使用する式を書く…というのでは思考が中断されてしまいますthat would cause a mental block.私はときどき,#defineを,最初に使用する行の直前で定義しておくことがあります.そしてあとで,通常の位置,すなわちファイルの先頭のほうに移動するわけです.
ところでこのLINE_BYTE_MAXの意図ですが, 標準入力から1行の入力を得ようとするとき,何バイトになるかは,プログラム(実行プロセス)側からは分かりません.そこで,fgets関数で最大LINE_BYTE_MAXバイトまで読み出す,と上限を設けています.常にそのバイト数まで読み出される,というものではなく,途中に改行文字があれば,そこまでです.「+ 1」は,'\0'を格納するためです.
fgetsの呼び出しにより,改行文字も,配列lineに格納されますが,改行文字は後の処理で使用しませんので,if文の中で(ある条件のもとで)「line[strlen(line) - 1] = '\0';」として,削除しておきます.Cの文字列処理で,ある文字以降を取り除くには,このように,取り除きたい位置(の先頭)に'\0'を代入するだけです.
今日はこれで妥協しますが,まだ問題があります.上記のコードでは,1行またはLINE_BYTE_MAXバイトを読み出すだけですが,今後,1行が長くなったときに,「ユーザが与えた長い1行」を,計算機内部では,2回に分けて読み出す,すなわち「2行と見なして読み出す」という危険があります.明日しっかりと対策をとりましょう.
ともあれ,プレイヤー名を標準入力から受け取り,空っぽの表を出力するプログラムが出来上がりました.コンパイルして,実行します.LinuxとGCCなら,コンパイルには「gcc -o roundrobin roundrobin.c」でしょう.実行コマンドですが,「echo WNM | ./roundrobin」というのが便利です.これは,標準入力から値を入力することなく,すぐ結果が出ます.「WNM」のところを変えて実行すれば,簡単に,いろいろな表が出てくるはずです*2.