わさっきhb

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

構造体プログラミングの実例(6)〜いろいろ実行,そして修正

前回が終わった時点でのソースコードを確認しましょう.

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

/* 標準入力から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;
}

/* 文字列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;
}

/* 対戦結果を追加する */
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;
}

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

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

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

  return 0;
}

では実行しましょう.説明のため,実行環境などを以下の通りとします.

次のコマンドで,コンパイルをします.

gcc -o roundrobin roundrobin.c

実行は,こうですね.

./roundrobin

入力ですが,第2回で仕様を決めるときに書いた入力を,打ち込んでみましょう.

WNM

あっと,この時点で,中身が空の表が出てきます.いったんCtrl-Dを押してプログラムを終了させます.ソースファイルを見直してみると,main関数にある,「print_table(tab);」のひとつ*2が不要なことに気づきます.取り除いてコンパイル,実行して,入力をやり直しましょう.

gcc -o roundrobin roundrobin.c
./roundrobin
WNM
W>N
W>M
M>N

何も出ません.そうでした,「!」に与えないと,出力しないのでした.では…

!
 WNM
W-oo
Nx-x
Mxo-

ちゃんと表が出ました.この入力はここまでなので,Ctrl-Dを押して,終了しましょう.
別の入力を試しましょう.総当たりプログラムを書くきっかけになった,とあるWebページの対戦結果を,打ち込んでみます.

./roundrobin
ABCDEF
A>B
B>C
D>C
E>C
F>C
A>C
D>A
A>E
E>F
F>A
B>F
F>D
E>D
D>B
B>E
!

そうすると,出力は,

 ABCDEF
A-ooxox
Bx-oxoo
Cxx-xxx
Dooo-xx
Exxoo-o
Foxoox-

となりました.期待通りの表です.ここでもCtrl-Dで終了させます.以下,「Ctrl-D」をどこで押すかについては省略します.
ではここから,もっといろいろな入力を投入しましょう.正常でない入力に対して,どんな出力になるのかを見ます.そうして仕様の抜けを見つけるとともに,必要に応じて,仕様とソースコードを修正することにします.
この作業はできれば,コードを書き,修正していく人と,様々な入力を試みる人とを別にしたいものです.すると,いろいろ困ったバグが目に見えてくるbe exposedものです.一人でするなら,とにかくいろんな角度,視点で「入力」を考えましょう.
まず,「3人なら3回の対戦で終わらないといけないのに,このプログラムでは回数制限がない」という点に注目して,すでに結果が記録されているのに,同一対戦の結果をまた送るとどうなるか,見てみましょう.

./roundrobin
WNM
W>N
W>M
M>N
!
 WNM
W-oo
Nx-x
Mxo-

ここまでは前のと変わりません.続けて

M>N
!

とすると…同じ表になりました.さらに,

N>M
!

と,勝敗を反対にするとどうでしょう….

 WNM
W-oo
Nx-o
Mxx-

表が変化しました.これって…結果の改ざん?
このように,結果を後で変更できるようにしていいのか,ですが,実用上も,勝敗を記録したあとで,実は勝者と敗者が逆だったので訂正したいというのは,起こり得るのではないでしょうか.ということで,プログラムとしては,この入力を許容することにします.そして仕様に,『「α>β」という形の入力に対して,すでにα対βの対戦結果が登録されていれば,その結果を破棄して新たに記録する』というのを追加しましょう.
別の種類の「不正な入力」を与えます.

./roundrobin
WNM
W>W
!
 WNM
Wx
N -
M  -

これは,まずいですねえ.「α>α」の形の行,言い換えると,勝者と敗者が同一という入力があってはいけません.仕様を見直すと,

「α>β」(ただし,αとβは1行目にある文字とする)があれば…

とありましたが,これを

「α>β」(ただし,αとβは1行目にある,相異なる文字とする)があれば…

に訂正しましょう.
ソースファイルも修正します.関数add_resultを見直します.winnerは勝者の位置で,loserは敗者の位置ですから,

  if (winner < 0 || loser < 0) {

  if (winner < 0 || loser < 0 || winner == loser) {

とします*3.そうして,コンパイル・実行すると,

./roundrobin
WNM
W>W
W>W: ignored.

これで,入力は不正なものと認識され,無視されました.
「勝者と敗者が同じでいいの?」から,次の疑問も浮かび上がります…「1行目のプレイヤーに,『WNMW』のように重複があったら?」
試してみましょう.

./roundrobin
WNMW
!
 WNMW
W-
N -
M  -
W   -

表は出てきます.でもおかしいですね.ひとつ,勝敗を入れてみますか.

W>N
!
 WNMW
W-o
Nx-
M  -
W   -

うーん,後者の「W」には結果が書けないようです.
そもそもこんなふうに,プレイヤー名に重複があれば,プログラムが見つけて「そんな入力,ダメですよ」とエラーメッセージを発し,終了すべきでしょう.プログラムを修正します.
どこを見るかというと,initialize関数です.引数playerについて,2つのif文で,仕様に合わない条件を排除していますが,さらに条件を付け加えます.

  if (has_same_player(player)) {
    printf("A player appears twice.\n");
    exit(1);
  }

いきなり,未定義の関数has_same_playerを呼び出しています.慣れないうちは,関数ではなく「文字列の中に,文字の重複がある」というのを判定する処理を書いてしまいがちですが,処理が簡単でなさそうに思えたら,関数名を決めて呼び出すコードを書く習慣をつけましょう.もちろんその後すぐ,その関数を定義します.そうすることで,見通しよくプログラムを書けるようになります.
さて,「文字列の中に,文字の重複がある」というのを判定する関数ですが,「i≠jでかつplayer[i]=player[j]」を見つればいいことに注意して,コードを示します*4.initialize関数のすぐ上に書きます.

/* 文字列playerの中に同一文字が2つ以上あるか判定する */
int has_same_player(char *player)
{
  int i, j;

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

  return 0;
}

これで問題解決のはずです.コンパイルして,入力を与えてみましょう.

./roundrobin
WNMW
A player appears twice.

ちゃんと,エラーで終了してくれています.確認のため,正しい入力も.

./roundrobin
WNM
!
 WNM
W-
N -
M  -

これでよさそうです.安心する前に,仕様を書き換えておきましょう.『入力の1行目は,全プレイヤーの名前を並べる.文字数(プレイヤー数)は,2以上20以下とする.』の直後に,『各文字は相異なるものとする.』を追加します.
異常な入力としてはほかに,入力の字数がうんと少なかったり,うんと長かったりするのがありますが,いずれも不正な入力とみなしてくれます.
ということで,完了です.おつかれさまでした.
…なのですが,ここで終わったら,プログラミング能力は進歩しません.日ごろの心がけとして,あと2つ,やっておきたいことがあります.次回と次々回で取り上げましょう.

*1:ソフトウェア名は大文字の「GCC」,コンパイル時のコマンドは小文字の「gcc」です.

*2:どちらかは,分かりますよね?

*3:あるいは,変数宣言直後のif文に,「|| line[0] == line[2]」を追加しても,同じ動作になります.

*4:ただしこれは,鈍くさい書き方です.Cらしい書き方は,「int i, j;」ではなく,2つのcharポインタ変数に置き換えて,2重のforループで回すことですね.いや,forループとstrchr関数を組み合わせれば,charポインタはひとつだけで済むなあ….