第42回
仕様設計からコーディングまで~タブ/スペース変換プログラムを作る(2)

構造体でパラメータを整理する

今紹介したプログラムは、もちろん正常に動作します。しかし、将来の改造や修正を考えた場合、あまり良いソースとは言えません。先のことも考えて「手直ししやすい」ソースを書くことは重要です。

複数のデータをまとめる

上のソース(ex4201.cのmain関数――リスト1)では、コマンドラインで指定された「タブストップ幅」と「入力ファイル名」に、main関数の引数であるchar型ポインタの配列"argv[]"の値をそのまま使っています。

このプログラムは、最終的に「複数のファイルを指定できる形」に改良する予定です。その場合、複数のファイルに対してそれぞれ異なるタブストップ幅を指定できるようにすれば、使い勝手がよくなるでしょう。

すると、ファイル名とタブストップ幅の組み合わせを複数処理しなければならなくなります。ところが、main関数では引数であるポインタの配列"argv[]"を直接使っているため、その何番目にどの値が入っているのかを把握するのが困難になります。

最終的な仕様に改良することを考えれば、ファイル名とタブストップ幅の組み合わせをもっと分かりやすい変数に格納しておいた方がよいでしょう。

配列でも対応できるが……

単にint型(タブストップ幅)とchar型配列(ファイル名)の変数を複数用意することも考えられますが、後の改良を考えればそれだけでは不十分です。特に、複数のタブストップ幅とファイル名の組み合わせを保存することを目指すなら、もう少し合理的なデータ構造を用意するべきです。

配列を用意することも考えられます。仮に組み合わせを8組まで指定できるようにするなら、
int  tab[8];
char *fname[8];
として、配列の添字(要素番号)をそれぞれ
タブストップ幅:tab[n]
ファイル名:fname[n]
のように対応させればよいことになります。

構造体が合理的

上の方法でも、小規模なプログラムなら2つの配列の対応を把握することは難しくありません。しかし、パラメータの組み合わせが多くなってくると、複数の配列を関連付けることが難しくなってくるでしょう。また、配列の要素数はあらかじめ決めておかなければならないので、その時々で変化するコマンドライン・オプションの数に対応させることも難しくなります。

上のような場合を想定すれば、「タブストップ幅」と「ファイル名」という関連した情報をひとまとめにして、構造体で管理するのが最善です。

今回作るプログラムなら、以下のようにしてまず_CONV型の構造体を定義します。
typedef struct {
    int    tab;
    char   *fname;
} _CONV;

次に、以下のようにして_CONV型の構造体変数convを宣言します。
_CONV	conv;

将来の改良にも有利

今の段階では、タブストップ幅とファイル名の組み合わせは1つだけですが、このようにしておけば後に複数の組み合わせを対象とする場合でも、構造体のポインタを使ったリスト構造を用意することで、容易に対処できるようになります。

では、構造体を使ってプログラムを書き直してみましょう。パラメータから読み取ったタブストップ幅とファイル名をまとめた構造体は、ポインタで扱うようにしています。タブをスペースに変換するtb2sp関数には変更がありません。

ただ、残りの処理をすべてmain関数に集中するとソースが長くなり、わかりにくくなるため、コマンドライン・パラメータ(main関数の引数)を構造体に保存するsetdata関数を新たに用意し、それをmain関数から呼び出す形にします。

また、パラメータを調べてそれが正しい場合にエラーメッセージを表示する処理も、main関数から外してargchk関数として定義しています。

構造体の定義などはリスト2、setdata関数はリスト3、argchk関数はリスト4、main関数はリスト5のようになります。

リスト2:タブストップ変換プログラム(構造体版)の記号定数と構造体の定義部分
#include  <stdio.h>
#include  <stdlib.h>
#include  <jctype.h>

#define  ARGMIN      3
#define  TRUE        1        /* 真 */
#define  FALSE       0        /* 偽 */
#define  BUFSIZE     1024     /* バッファサイズ */
#define  MIN_TAB     4        /* 最小タブストップ幅 */
#define  MAX_TAB     16       /* 最大タブストップ幅 */
#define  DEFAULT_TAB 8        /* 標準タブストップ幅 */
#define  SPACE       0x20
#define  TAB         0x09
#define  ERR         (-1)     /* エラーコード */

/* 構造体を定義 */
typedef  struct {
  int  tab;    /* タブストップ幅 */
  char *fname; /* 入力ファイル名 */
  FILE *fp;    /* ファイルポインタ */
} TabConv;

リスト3:タブストップ変換プログラム(構造体版)のパラメータを構造体に設定する部分~setdata関数
int  setdata(TabConv *p)
{
  if (p->tab < MIN_TAB)
    p->tab = DEFAULT_TAB;
  else if (p->tab > MAX_TAB)
    p->tab = MAX_TAB;
  if ((p->fp = fopen(p->fname, "r")) == NULL) {
    fprintf(stderr, "%s がオープンできません.¥n", p->fname);
    return (FALSE);
  }
  return (TRUE);
}

リスト4:タブストップ変換プログラム(構造体版)のパラメータをチェックするargchk関数
/* パラメータの数が最少数以下ならエラーメッセージを表示する */
int  argchk(int argc)
{
  if (argc < ARGMIN) {
    fputs("TABCONV : タブストップ幅(1-16)とファイル名を指定してください.",
           stderr);
    return (FALSE);
  }
  return (TRUE);
}

リスト5:タブストップ変換プログラム(構造体版)のmain関数
/* main関数 */
void  main(int argc, char *argv[])
{
  char    rbuf[BUFSIZE + 1]; /* 読み込みバッファ */
  char    wbuf[BUFSIZE + 1]; /* 書き出しバッファ */
  TabConv indata, *p;        /* TabConv型の変数を定義 */

  p = &indata;        /* ポインタを初期化し、構造体のデータを
                         ポインタで扱えるようにする */
  if (argchk(argc) == FALSE)
      exit(ERR);
  p->tab = atoi(argv[1]);    /* オプション文字を数値に直す */
  p->fname = argv[2];
  if (setdata(p) == FALSE)
      exit(ERR);
  fprintf(stderr, "タブストップ間隔 : %d¥n", p->tab);
  while (fgets(rbuf, BUFSIZE, p->fp) != NULL) {
     tb2sp(rbuf, wbuf, p->tab);
     fputs(wbuf, stdout);
  }
  fprintf(stderr, "¥a¥t---- 変換終了しました!!¥n");
}