わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

構造体プログラミングの実例(4)〜構造体の中身を見る

昨日の時点で,ソースコードは,以下の通りです.

#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行と見なして読み出す」という危険があります.明日しっかりと対策をとりましょう.
ともあれ,プレイヤー名を標準入力から受け取り,空っぽの表を出力するプログラムが出来上がりました.コンパイルして,実行します.LinuxGCCなら,コンパイルには「gcc -o roundrobin roundrobin.c」でしょう.実行コマンドですが,「echo WNM | ./roundrobin」というのが便利です.これは,標準入力から値を入力することなく,すぐ結果が出ます.「WNM」のところを変えて実行すれば,簡単に,いろいろな表が出てくるはずです*2

*1:Tokyoを,同じルールで省略すると,TKYになります.Osakaは,SKではなく,OSKです.単語の先頭の母音は,省略しません.

*2:1行入力なら「echo 値 | ./roundrobin」ですが,ファイル入力なら,「cat ファイル名 | ./roundrobin」または「./roundrobin < ファイル名」というコマンドが有用です.