わさっきhb

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

構造体プログラミングの実例(5)〜他の関数の定義

前回が終わった時点でのソースコードは以下の通りです.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PLAYER_MAX 20

enum result { /* 勝敗情報を表す列挙型 */
  RSLT_NOTPLAYED, /* 未対戦 */
  RSLT_WIN,       /* 勝 */
  RSLT_LOSE,      /* 負 */
  RSLT_NA         /* (同一プレイヤー同士なので)対戦しない */
};

typedef struct {
  char player[PLAYER_MAX + 1];
  int table[PLAYER_MAX][PLAYER_MAX];
  int player_number;
} rrtable;

rrtable *initialize(char *player)
{
  rrtable *tab;
  int i;

  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, RSLT_NOTPLAYED, PLAYER_MAX * PLAYER_MAX);
  for (i = 0; i < tab->player_number; i++) {
    tab->table[i][i] = RSLT_NA;
  }

  return tab;
}

void print_table(rrtable *tab)
{
  int i, j;

  printf(" %s\n", tab->player);

  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');
  }
}

int main(void)
{
#define LINE_BYTE_MAX 25
  char line[LINE_BYTE_MAX + 1];
  rrtable *tab;

  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);
  print_table(tab);

  return 0;
}

では完成に向けて,進めていきましょう.…まずは,昨日の懸念から.

上記のコードでは,1行またはLINE_BYTE_MAXバイトを読み出すだけですが,今後,1行が長くなったときに,「ユーザが与えた長い1行」を,計算機内部では,2回に分けて読み出す,すなわち「2行と見なして読み出す」という危険があります.

これについて,1行(改行まで)は1行とみなして読み出す関数が必要です.といっても,fgetsでは,第2引数に読み出しバイト数を指定するので,うまくいきません.標準入力だからgetsを使いますか? いやいや,配列lineの大きさを超える入力が来ると,まずいです.メモリの中身がおかしくなってしまいます.
ここは独自に関数を作りましょう.こうします.

/* 標準入力から1行読み出して,lineの参照する配列に格納する */
char *get_line(char *line, int max_byte)
{
  int c;
  int i;

  /* 1文字ずつ読み出して,lineの配列に格納する */
  for (i = 0; i < max_byte && (c = getchar()) != EOF && c != '\n'; i++) {
    line[i] = c;
  }
  line[i] = '\0';

  /* 改行になるまで読み飛ばす */
  if (i == max_byte) {
    while ((c = getchar()) != EOF && c != '\n')
      ;
  }

  /* 1文字も読み出せなければNULLを返す */
  if (line[0] == '\0') {
    return NULL;
  }
  return line;
}

何をしているかは,コメントに書いた通りです.要所は3つあって,まず,ライブラリ関数には,標準入力から1文字を読み出すgetchar関数を使うこと.2番目は,getcharを反復の中で呼び出し,反復を終える条件は,EOFになったか,または改行文字を読み終えた時点とすること.そして最後に,1行の字数が多すぎて配列領域に格納できなくなっても,EOFまたは改行文字になるまで1文字ずつ読む(読み飛ばす)こと,です.
もうひとつ,押さえておく点があります.max_byteと,1行のバイト数のどちらが大きいかに関わらず,改行文字は,lineの指す配列領域には格納されません.言い換えると,この関数を通じて,改行文字を取り除くようにしています.
戻り値にも注意を払います.「成功すれば第1引数,失敗すればNULL」として*1,この値を,呼び出し側で真偽判定に使えるようにします.if文の式にポインタ値が使えて,NULLのときのみ偽,それ以外の値は真になります.
呼び出し側というのは…mainの中です.mainの処理部を削除して(変数宣言とreturn文は残して),とりあえず以下のように書き,うまく入力が得られるか,確認してみます.

  /* 最初の行を読み出し,構造体を初期化する */
  get_line(line, LINE_BYTE_MAX + 1);
  tab = initialize(line);
  print_table(tab);

  /* 各行を読み出してlineに格納し,出力する */
  while (get_line(line, LINE_BYTE_MAX + 1)) {
    puts(line);
  }

コンパイルして実行し,1行が25字以内ならそのまま,超えると25字までに切られて出力されることを,確認しましょう.
ここで少し,コード書きから脱線します.
小規模なプログラミングの流れとして,まずデータ構造も関数呼び出しも全部作り,一通り書き上げたところで,コンパイルしてデバッグして,実行してデバッグして,完成させるというやり方があります.何を書けばいいかがすべて,頭の中に入っているのなら,この方法で「一気に書き上げる」のもいいでしょう.
しかし,初めのうちは順調だけど,電話とか来客とか,途中で少し飽きてインターネット上の面白情報にアクセスしたりして,そしてまたプログラミングに戻ると,何をすればいいかを忘れてしまいやすいです.
そこでみなさんにお勧めしたいのが,小さな動くプログラムから始めて,少しずつ機能をつけて実行して,その都度うまく動くのを確かめながら,完成に持っていくという方法です.こうすると,うまく動作しなかったとき,どこが原因かがつかみやすくなります.たいていは,新たに追加したコードですね.そうして,プログラミングの迷宮に閉じ込められる可能性が減ります.
ただしこのコーディングスタイルでも,何らかの理由で少し間を置いて,戻ってくると,何をしていたんだったか思い出せないという可能性があります.
これについては,記録をとる習慣をつけましょう.ノートに,何をしていくかを列挙して,解決したら「済Done」の印をつけるなり,横線を引くなりすればいいのです.紙がないときは…ソースファイルの末尾にコメントとして,何をすべきかを並べておきます.そして,実装した行を消す…のは,どこまだやったかを忘れてしまいがちです.実装した項目には,行頭に「■」のような記号をつけるほうが,進捗管理しやすいでしょう.
さて思考をプログラミングに戻して…次はいよいよ,対戦結果を追加する関数ですね.「α>β」の形で情報を受け取ったときに,αはβに勝ち,βはαに負け,という情報を,構造体の中の2次元配列に格納する処理です.
この関数の引数は,どうしましょうか.第1引数は構造体のポインタで決まりです.あとの引数について,2つの案が考えられます.ひとつは,「第2引数は勝ちプレイヤーの文字,第3引数は負けプレイヤーの文字」とするもの,もうひとつは,第2引数を,入力として受け取った文字列そのもの(get_lineで配列に格納した文字列)とするものです.
ここは後者を採用し,引数は結局,「第1引数はrrtableのポインタ,第2引数は入力文字列」とします.入力が適切かどうかを関数の中でチェックすることにします.呼び出し側がすっきりします.
では関数定義です.

/* 対戦結果を追加する */
void add_result(rrtable *tab, char *line)
{
  int winner, loser;

  if (strlen(line) != 3 || line[1] != '>') {
    printf("%s: ignored.\n", line);
    return;
  }

  winner = search_player_index(tab, line[0]);
  loser = search_player_index(tab, line[2]);

  if (winner < 0 || loser < 0) {
    printf("%s: ignored.\n", line);
    return;
  }

  tab->table[winner][loser] = RSLT_WIN;
  tab->table[loser][winner] = RSLT_LOSE;
}

ここは,関数の中にコメントをつけず,コードを見れば何をしているかが分かるように,変数名や処理を工夫してみました.
search_player_indexは,まだ定義していません.これは,tabすなわちrrtable構造体のポインタと,プレイヤーを表す1文字を入力にとり,その1文字が,プレイヤー文字列tab->playerの何文字目にあるかを返す関数,というつもりです*2.先頭は0文字目です.見つからなかったら,-1を返すことにします.「文字」から「位置」への変換をする関数を通すことで,「tab->table[勝ちプレイヤーの位置][負けプレイヤーの位置] = 勝ち;」と書けるようになります.
search_player_indexも,定義してしまいましょう.まだ関数プロトタイプは書いていません*3ので,この関数search_player_indexは,add_result関数より上で記述する必要があります.

/* 文字列playerから文字cを探してその位置を返す */
int search_player_index(rrtable *tab, char c)
{
  int i;
  char *player = tab->player;

  for (i = 0; i < strlen(player); i++) {
    if (c == player[i]) {
      return i;
    }
  }

  /* 見つからなければ負の数を返す */
  return -1;
}

ここでは,for文を使って,1文字ずつ探索するようにしました.代わりに,ライブラリ関数のstrchrを用いても書けます.その場合,ループ用の変数iはなくなりますが,strchrの戻り値を格納するポインタ変数を宣言しておくことになります.ポインタの減算が好きな人は,こちらをどうぞ.
ともあれ,add_resultも完成しました*4.これをmainから呼び出しましょう.上述のコメントの「/* 各行を読み出してlineに格納し,出力する */」,そして直後のwhileブロックを削除して,こう書きます.

  /* 各行を読み出し,表を埋めていく */
  while (get_line(line, LINE_BYTE_MAX + 1)) {
    if (line[0] == '!') {
      print_table(tab);
    } else {
      add_result(tab, line);
    }
  }

これでプログラムは出来上がりました.動作確認は明日にしましょう.

*1:この値の返し方は,文字列処理のライブラリ関数ではよく使われるもの…といったん書いて,ライブラリ関数を調べ直すと,そういう戻り値になる関数はなさそうですね.strcatは常に第1引数を返すし,strchrは,見つかったら(第1引数ではなく)その位置だし.

*2:いきなりこんな関数を思いつかないかもしれません.そういうときは,add_resultの中で,tab->playerとline[0]を用いて勝者の位置を求める処理を書きます.コピー&ペーストして少し修正し,tabとline[2]を用いて敗者の位置を求める処理を書きます.2つの処理は余りにも似通っているはずで,それを共通化するような関数を定義して,呼び出すようにすれば,結局,同じものができます.

*3:関数プロトタイプは,次々回につけます.そのときに,なぜ完成するまで関数プロトタイプを書かないかを明かす予定です.

*4:ただし今のコードには,バグがあります.明日,潰します.