第45回
複数のソースファイルからプログラムを作る~マルチモジュール開発の基本

ソース分割の実例

これまでに作成してきた「タブ←→スペース変換プログラム」を複数のソースに分割してみましょう。

タブ/スペース変換プログラムを分割する

元のソースは前回紹介した“ex4401.c”で、全体の構造にも各関数の処理にも変更はありません。

分割した各ソースファイルでは、ほかのソースファイルで定義された関数を参照することになるため、独自のヘッダファイルを作成し、その中でいくつかの関数をextern宣言する必要があります。また、記号定数の定義と変換の指示を保存する構造体の定義も各ソースで共通するため、その部分もヘッダファイルに記述します。

元のソースに記述された内容を、役割別に分割してみましょう。

メイン処理

main関数とそこから直接呼び出される関数のうち、さらに下請け関数を呼び出すなど1つの処理が複数の関数に分かれているような関数を除き、わざわざ別モジュールとする必要のない関数をここにまとめます。

以下の2つの関数が該当します。
使用方法の表示~putusage関数
構造体の初期化~convinit関数

メイン処理をまとめたソースファイルの名前は、プログラム名と同じにするのが一般的です。ここでは完成したプログラムを“stconv”とするため、ソースファイルは“stconv.c”とします ※2 (リスト1)。

サンプルのプログラムでは“ex4501”などの名称を用いてきましたが、今回は複数のソースから1つのプログラムを生成するため、分かりやすいファイル名を用いることにします。
リスト1:メイン処理~stconv.c
/* ------------------------------------------------------------ *
 * stconv.c     テキストファイルのスペース<-->タブ変換          *
 * 書式 : stconv option <input file> <option> <input file> ...  *
 *        option の説明                                         *
 *         -Tn : スペースを「タブコード」に変換                 *
 *               1バイトだけのスペースはタブに変換されない     *
 *         -Sn : タブコードを「スペース」に変換                 *
 *        オプションに続く'n'でタブストップの間隔を指定         *
 *        <input file> で入力ファイルを指定                     *
 * ------------------------------------------------------------ */

#include    <stdio.h>
#include    <stdlib.h>
#include    "stconv.h"

/* ----------------------------------------------------------- *
 * putusage : 使用方法を表示                                   *
 * ----------------------------------------------------------- */
void    putusage(void)
{
  fputs("STCONV : タブ/スペース変換フィルタ¥n",
         stdout);
  fputs("¥x7¥tSTCONV option <input file> <option> <input file> ...¥n", stdout);
  fputs("¥t¥t---- <option> の説明 ----¥n", stdout);
  fputs("¥t-Tn   : スペースを「タブコード」に変換します。¥n", stdout);
  fputs("¥t        1バイトだけのスペースはタブに変換されません。¥n", stdout);
  fputs("¥t-Sn   : タブコードを「スペース」に変換します。¥n", stdout);
  fputs("¥tオプション文字に続く n でタブストップの間隔を指定します。¥n", stdout);
  fputs("¥t<input file> で入力ファイルを指定します。¥n", stdout);
  fputs("¥t入力ファイルに<option>を指定しない場合は、それ以前に指定された¥n", stdout);
  fputs("¥tオプションを引き継ぎます。¥n", stdout);
  fputs("¥t処理結果の出力は標準出力になっているので、リダイレクトしてください。¥n¥n", stdout);
}

/* ----------------------------------------------------------- *
 * convinit : 構造体メンバの初期化                             *
 * ----------------------------------------------------------- */
void    convinit(STCONV *p)
{
  p->sw = 0;
  p->tab = DEFAULT_TAB;
  p->fname = NULL;
  p->fp = NULL;
}

/*------------------------------------------------------------ *
 *                                                             *
 *                           main                              *
 *                                                             *
 *------------------------------------------------------------ */
void    main(int argc, char *argv[])
{
  int     i, count;
  STCONV  dat[ARGMAX], *p;

  if (argc < ARGMIN) {
    putusage();
    exit(1);
  }
  p = dat;
  for (i = 0; i < ARGMAX; i++)
    convinit(p);    /* 構造体の初期化 */
    /* コマンドラインを解析し、構造体の配列"dat[]"へオプションと
       ファイル名へのポインタをセット */
  if ((count = chk_opt(p, argv)) == 0) {
    fprintf(stderr,"出力するファイルがありませんでした.¥n");
    exit(1);
  }
  /* 構造体の配列を順次処理 */
  for (i = 0; i < count; i++, p++) {
    if (setdata(p) == FALSE)
      continue;
      stconv(p);
      putchar(_FF);   /* プリンタ出力のためにページ送りする */
  }
}
/* -------- end of file -------- */

データの初期化処理

コマンドラインを解析して変換方法を構造体の配列に設定していく処理と、その内容からファイルをオープンしてタブストップ幅を適正値に設定する処理をここにまとめることにします。

以下の2つの関数が該当します。
コマンドラインの解析と構造体のメンバ設定~chk_opt関数
ファイルのオープンとタブストップ幅の調整~setdata関数

ソースファイル名は“datinit.c”とします(リスト2)。

リスト2:データの初期化処理~datinit.c
/* ------------------------------------------------------------ *
 * datinit.c                                                    *
 *  コマンドラインの解析と構造体の設定                          *
 * ------------------------------------------------------------ */

#include    <stdio.h>
#include    <stdlib.h>    /* exit、atoi関数 */
#include    "stconv.h"

/* ----------------------------------------------------------- *
 * setdata : 構造体のメンバをセット                            *
 *   構造体のメンバtabの値をチェックし、妥当な値をセットする。 *
 *   fnameをに基づいてファイルをオープンし、ファイルポインタ   *
 *   をfpにセットする--エラーならFALSEを返す。                 *
 * ----------------------------------------------------------- */
int     setdata(STCONV *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);
  /* この段階ではファイルはオープンされたままなので
     処理後にクローズすること */
}

/* ----------------------------------------------------------- *
 * chk_opt : オプションを解析して構造体にデータをセット        *
 *   p      : 構造体                                           *
 *   argv   : コマンドライン文字列                             *
 * ----------------------------------------------------------- */
int     chk_opt(STCONV *p, char *argv[])
{
  int   i, sw, tabsize;
  char  * arg_p;

  /* 解析をARGMAX回繰り返す */
  for (i = 0; (i < ARGMAX) && (*(++argv) != NULL); ) {
    arg_p = *argv;
    /* オプション文字列を処理 */
    if (*arg_p == '-') {
      arg_p++;
      switch (*arg_p) {
        case 't' :
        case 'T' :  /* SPACE -> TAB */
                   sw = 1;
                   break;
        case 's' :
        case 'S' :  /* TAB -> SPACE */
                   sw = 0;
                   break;
        default  :
                   fprintf(stderr, "-%c は無効なオプションです.¥n", *arg_p);
      }
      arg_p++;
      tabsize = atoi(arg_p);
    }
    else {
      p->sw = sw;
      p->tab = tabsize;
      p->fname = arg_p;   /* ファイル名をコピー */
      p++;
      i++;
    }
  }
  return (i);		/* 有効な構造体数を返す */
}
/* -------- end of file -------- */

データの変換処理

実際にタブとスペースの変換処理を行う関数をまとめます。

以下の3つの関数が該当します。
main関数から直接呼び出される変換処理~stconv関数
stconv関数から呼び出される「タブ→スペース」変換処理~tb2sp関数
stconv関数から呼び出される「スペース→タブ」変換処理~sp2tb関数

ソースファイル名は“convert.c”とします(リスト3)。

リスト3:データの変換処理~convert.c
/* ------------------------------------------------------------ *
 * convert.c                                                    *
 *  タブ<-->スペースの変換関数                                  *
 * ------------------------------------------------------------ */

#include    <stdio.h>
#include    <jctype.h>    /* 日本語2バイトコードの処理用 */
#include    "stconv.h"

/* ----------------------------------------------------------- *
 * tb2sp : タブ->スペース 変換                                 *
 *   src  : 送り側文字列                                       *
 *   dest : 受け側文字列                                       *
 *   tab  : タブストップ数                                     *
 * ----------------------------------------------------------- */
void    tb2sp(char *src, char *dest, int tab)
{
  int i, j, step;

  /* 送り側文字列の終端まで繰り返す */
  for (i = 0; *src != '¥0'; i++) {
    /* タブコードが見付かったらスペースに変換 */
    if (*src == TAB) {
      /* 2バイト日本語への対処 */
      if ((i > 0) && (iskanji(*(src-1))))
        *dest++ = *src++;
      else {
        /* タブコードをスペースに置き換える */
        step = tab - (i % tab);
        for (j = 0; j < step; j++)
          *dest++ = SPACE;
        src++;
        i += (j - 1);
      }
    }
    else
      *dest++ = *src++;
  }
  *dest = '¥0';
}

/* ----------------------------------------------------------- *
 * sp2tb : スペース->タブ 変換                                 *
 *   src  : 送り側文字列                                       *
 *   dest : 受け側文字列                                       *
 *   tab  : タブストップ数                                     *
 * ----------------------------------------------------------- */
void    sp2tb(char *src, char *dest, int tab)
{
  char  *pos;
  int   i, j;
  int   step;

  /* 送り側文字列の終端まで繰り返す */
  for (i = 0; *src != '¥0'; i++) {
    step = tab - (i % tab);
    /* スペースが見付かったらタブコードに変換 */
    if (*src == SPACE)  {
      /* 2バイト日本語への対処 */
      if (((i > 0) && (iskanji(*(src-1)))) || (*(src+1) != SPACE))
        *dest++ = *src++;
      else {
        /* スペースをタブコードに置き換える */
        pos = src;
        for (j = 0; j < step; j++, pos++)
          if (*pos != SPACE)
            break;
          if (j == step) {
            *dest++ = TAB;
            src = pos;
            i += (j - 1);
            /* iはfor文の中でインクリメント
               されているため、このときだけ -1 しておく */
           }
           else
             *dest++ = *src++;
      }
   }
   else {
     *dest++ = *src++;
   }
  }
  *dest = '¥0';
}

/* ----------------------------------------------------------- *
 * stconv : ファイルを読み込んで1行ずつ変換                    *
 *   p      : 構造体                                           *
 * ----------------------------------------------------------- */
void    stconv(STCONV *p)
{
  char rbuf[BUFSIZE + 1];
  char wbuf[BUFSIZE + 1];

  /* リダイレクト時でもディスプレイに出力されるよう stderr に送る */
  fprintf(stderr, "%s¥n", p->fname);
  fprintf(stderr, "¥tタブストップ間隔 : %d / ", p->tab);

  if (p->sw == 0) {
    fprintf(stderr, "タブ → スペース¥n");
    while (fgets(rbuf, BUFSIZE, p->fp) != NULL) {
      tb2sp(rbuf, wbuf, p->tab);
      fputs(wbuf, stdout);
    }
    fclose(p->fp);
  }
  else {
    fprintf(stderr, "スペース → タブ¥n");
    while (fgets(rbuf, BUFSIZE, p->fp) != NULL) {
      sp2tb(rbuf, wbuf, p->tab);
      fputs(wbuf, stdout);
    }
    fputs("---- end of file ----", stdout);
    fclose(p->fp);
  }
}/* -------- end of file -------- */

ヘッダファイルのパス

リスト1~3を見ると、ソースの先頭でstdio.hなど標準関数を扱うためのヘッダファイルのほかに、“stconv.h”というヘッダファイルが取り込まれていることが分かります。これがこのプログラム独自のヘッダファイルで、先述したように記号定数と構造体の定義と関数のextern宣言を記述します(リスト4)。

すでに説明したように、ヘッダファイルは#includeプリプロセッサ指令で取り込みます。このとき、stdio.hやstdlib.hなど処理系にあらかじめ備わっている標準関数用のファイルは、名前を< >で囲みます。これはコンパイラが
取り込むファイルの場所を
規定のパスを参照して探す
という動作を示します。

ヘッダファイルのフォルダ

通常、プログラム独自のヘッダファイルは、プログラムの .cソースファイルと同じフォルダ、またはそのフォルダ内の“include”や“header”などの名前を付けたサブフォルダに保存します。当然、規定のパスでは示されていません。

そういった規定のパス以外の場所にあるファイルは" "で囲んで指定します。#include指令の記述された .cソースファイルと同じ場所にあるなら、コンパイル時にそのフォルダをカレントにした状態で、パスを省略して
#include "stconv.h"
のように記述できます。

サブフォルダ“include”に保存している場合なら
#include "include¥stconv.h"
のように相対参照パスで記述できます。

もちろんフルパスを記述しても構いませんが、記述が長くなってしまうためミスを招きやすくなります。

リスト4:独自のヘッダファイル~stconv.h
/* ------------------------------------------------------------ *
 * stconv.h                                                     *
 *  テキストファイルのスペース<-->タブ変換 stconv 用定義        *
 * ------------------------------------------------------------ */

/* 記号定数の定義 */
#define     ARGMIN          2        /* オプションの最小数 */
#define     ARGMAX          8        /* オプションの最大数 */
                                     /* これ以下はエラーとする */
#define     BUFSIZE         1024     /* 文字列バッファのバイト数 */
#define     DEFAULT_TAB     8        /* 標準のタブストップ */
#define     MIN_TAB         2        /* 最小のタブストップ */
#define     MAX_TAB         16       /* 最大のタブストップ */

#define     SPACE           0x20     /* スペースコード */
#define     TAB             0x09     /* タブコード */
#define     _FF             0x0c     /* 改ページコード */

#define     TRUE            1        /* 論理値--真 */
#define     FALSE           0        /* 論理値--偽 */

/* 構造体 STCONV を定義 */
typedef struct {
          int     sw;     /* 0:TAB->SPACE 1:SPACE->TAB */
          int     tab;    /* タブストップ幅 */
          char    *fname; /* 入力ファイル名 */
          FILE    *fp;    /* ファイルポインタ */
        } STCONV;
/*
  メンバ'fname'は単なるchar型ポインタで、パラメータから受け継ぐ。
  char型配列ではないことに注意!
*/

/* 外部関数の宣言 */
/* defined in 'datini.c' */
extern int     setdata(STCONV *);
extern int     chk_opt(STCONV *, char **);

/* defined in 'convert.c' */
extern void    tb2sp(char *, char *, int);
extern void    sp2tb(char *, char *, int);
extern void    stconv(STCONV *);
/* -------- end of file -------- */