わさっきhb

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

構造体プログラミングの実例(3)〜構造体の定義

昨日決めた仕様を列挙しておきます.1点,追加があるほか,全体としてひとつの仕様となるように,いくつか修正しています.

  • 入力は標準入力から,出力は標準出力へ.
  • ASCII文字を使う.表もASCII文字のみで(等幅フォントとする).
  • プレイヤー名も入力から受け取る.1プレイヤーの名前を,ASCIIの1文字で表す.
  • [追加]プレイヤー間の勝敗のみを記録・出力するものとし,その内容(何対何など)は考慮しない.
  • 入力の1行目は,全プレイヤーの名前を並べる.文字数(プレイヤー数)は,2以上20以下とする.
  • 入力の2行目以降の各行は,「!」または「α>β」のいずれかとする.
    • 「!」のとき,その時点での総当たり表を出力する.
    • 「α>β」(ただし,αとβは1行目にある文字とする)のとき,αはβに勝ち,βはαに負けとなるよう,勝敗を記録する.
  • 出力の1行目は,最初に1文字スペースのあと,全プレイヤーの名前を並べる.
  • 出力の2行目以降各行の最初の文字は,プレイヤーの名前で,2文字目以降は,1行目の列のプレイヤーとの対戦結果を1文字で表す.「o」は勝ち,「x」は負け,「-」は対戦しない,「 」(1文字スペース)は未対戦とする.
  • i≧2,j≧2に対して,出力のi行j列の文字と,j行i列の文字には対応関係がある.すなわち一方が「o」なら,もう一方は「x」となる.一方が「 」なら,もう一方も「 」となる.

この仕様をもとに,Cでプログラムを書く際のデータ構造を考えていきます.
プログラムの中で保持しておかなければならない情報として,「全プレイヤーの名前」と「対戦結果」が挙げられます.
全プレイヤーの名前は,「プレイヤーを表す文字の配列」か,「プレイヤーを表す文字からなる文字列」のいずれかとして保存するのが自然でしょう.ここで,「文字の配列」と「文字列」の違いは,末尾に'\0'がないかあるかです.一見どちらでもいいように思えますが,1行目の出力や,「αとβは1行目にある文字」の判定の際に,文字列のほうが都合がいいので,ここは文字列を採用します.
次に「対戦結果」をどのようにして保持するか,考えます.
入力は「α>β」の並びですが,この文字列をそのまま文字列変数に格納して,何行にも渡るのは配列で持っておく,というのは,使い道がありません.この考えは除外します.
ここは当然,対戦結果の表を,2次元配列として格納すべきでしょう.
ここで2点,質問が出るでしょうから,解決しておきます.一つは,構造体が必要なのか,もう一つは,配列をいつ確保するか,です.
まず構造体を使用することの是非について.実のところ,構造体がなくても,プログラムを書くことはできます.その際,ある関数は,プレイヤーの文字列を引数にとり,ある関数は,2次元配列を引数にとり,ある関数は,両方をとることになるでしょう.となると,プログラムが出来上がったときに,引数が煩雑になります.定義する側の仮引数も,呼び出す側の実引数も.
プログラムとしては,一つの「総当たり表」に関する情報を,参照したり出力したりしたいのに,関数を作ったり使ったりたびに,プレイヤー文字列がいるのか,2次元配列がいるのかを検討しないといけません.それと,2次元配列を引数にとる関数をいくつも定義するとなると,その配列の要素数を,仮引数として毎回記述することになります.これは相当な手間です.
では,構造体を採用するとどうなるでしょうか.そのデータに読み書きするとき,その構造体のポインタを第1引数としておくことで,関数定義の統一性を保つことができます.今後,複数の構造体を取り扱うプログラムを読んだり書いたりするときにも,関数プロトタイプから,何に対する手続き群を定義しているかが一目瞭然となります.そして,2次元配列の要素数も,構造体の中に押し込めることができます.総当たり表を表現する情報として,「プレイヤー文字列」「戦績2次元配列」以外の情報を付け加えても(後述の通り,実際に付け加えるのですが),関数プロトタイプは変更しなくてかまいません.
これらのことを一言で表現すると,「情報隠蔽」です.そう,この情報隠蔽こそが,構造体採用の大きなメリットなのです.
次の問題点は,配列の確保についてです.20人以下ということなので,プレイヤー文字列も,2次元配列も,構造体の中で最大数まで確保しておくことにします.ちなみに別案は,malloc系関数で確保するというものです.「配列の配列」の動的確保は,手間がかかるにも関わらず,確保後の処理の手間が減るわけではないので,ここでは見送ることにします.
さて構造体の定義をしましょう.テキストエディタで,roundrobin.cという名前のファイルを新規にオープンして,私は以下の通りタイプして,rrtable型を定義しました.rrtableは,「round robin table」の略です.

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

タイプしながら考えたのですが,プレイヤーの数は,メンバplayerの文字列長として求められるものの,別途,メンバとして持っておくほうが分かりやすいと思うようになり,変数を追加しています.
ところで,プレイヤーの最大は,最初からマクロにしています.最初の1回はこのまま「PLAYER_MAX」と書きますが,Emacsで2回目以降この長いマクロを入力する際には,「P」を打ってから,Alt-/ を打つだけで済みます.動的補完です.
じゃなかった,マクロをきちんと定義しておきましょう.

#define PLAYER_MAX 20

あと,プログラムの骨格を作る基本になるのも,書いておきましょう.これまでのと合わせて,それぞれどのように配置すべきかは,大丈夫でしょうか?

#include <stdio.h>
int main(void)
{
  return 0;
}

今日の最後は,構造体の初期化です.まあmainの中でrrtable型の変数(オブジェクト)を定義するとしても,総当たり表くらいのプログラムなら問題ありませんが,将来,様々な構造体プログラミングを効率よく組めるように,少し手間をかけた方法を紹介します.
簡単に言うと,「構造体を実体化instantiate・初期化して,そのポインタを返す関数」を定義します.さっそくですがコードを記載します.

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

この関数の処理は,コメントなしでも読めると思います.とはいえ書いておくと,関数内で使用する変数の宣言のあと,プレイヤー数が範囲内に収まっているか確認をして,構造体を確保し,初期化して,そのポインタを返しています.
mallocを含むif文は,「mallocの使用例」を本で調べれば,型名は違えど,たいてい載っているでしょう.まず構造体として使用できる領域の動的確保を試みます.そして,成功したら先頭アドレスをポインタ変数tabが指すようにし,失敗したらエラーメッセージを出力して終了します.
関数の中で構造体の初期化をする際,これはあまりにも有名な禁止事項ですが,関数内のauto変数のポインタを返すことは,してはいけません.関数内にstatic変数を定義して,そのポインタを返すのなら,問題ありませんし,このプログラムではそれでもかまいません*1が,他のプログラミングにも利用できることを重視し,mallocを使用しています.
memsetの呼び出しで,2次元配列の部分を0にしています.ただしこの初期化は妥当とはいえず,次回,変更することにします.
この関数の中で,exit,malloc,memsetというライブラリ関数を呼び出しているので,ソースファイルの先頭部分に適切なヘッダファイルをインクルードしておきましょう.

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

*1:1回のプログラムで,複数個,その構造体を使用したいときには,static変数ではうまくいきません.総当たり表プログラムでは,一つしか使用しそうにないので,支障ないわけです.