第35回
変数の通用範囲~内部広域変数と外部広域変数

単語検索プログラムを作る

テキストファイルの中から特定の単語を探し、その単語が見つかった行と先頭からの文字数、見つかった単語の合計数を表示するプログラムを作ってみましょう。1つのソースで構成するシングルモジュールと2つのソースに分けたマルチモジュールの2種類を作り、広域変数の扱いの違いを比べてみます。

プログラムの仕様

プログラムは"sfind"という名前とし、テキストファイルは標準入力からリダイレクトで受け取り、探す単語はプログラムへのパラメータで与える仕様とします。処理結果は標準出力へ出力します。
sfind <str>

たとえばabc.txtから"Windows"という単語を探すなら、以下のようにして起動します。
sfind Windows < abc.txt

なお、サンプルではソースファイルを"ex3502.c"、実行形式ファイルを"ex3502.exe"としています。

単語検索の仕組み

このソースはmainとsearchの2つの関数からできています。

mainではgets関数で標準入力から文字列を1行ずつ(改行まで)読み込み、search関数を呼び出して読み込んだ1行の中にコマンドライン・オプションで与えられた文字列(argv[1])があるかどうかを調べます(処理を簡略化するため、1行は1024バイト以内としています)。

もし文字列が見つかれば、printf関数でその行番号と見つかった位置の桁(先頭を1とした文字数)を表示します。

実際に文字列を見つけるのはsearch関数です。その中では、第1引数(標準入力から読み込んだ文字列)を先頭から1文字ずつ進めていき、2つの文字列が同じかどうかを調べる標準関数strncmpで第2引数(コマンドラインで指定された検索文字列)と同じ文字列があるかどうかを調べています。

strncmp関数は、以下のような書式で用います。
strncmp(<文字列1>, <文字列2>, <文字数>);

<文字列1>から<文字数>で指定した文字数分の文字列が、<文字列2>と同じかどうかを調べます。同じであれば0が返ってきます。これを利用して、第1引数の文字列sを1文字ずつ先へ進め、その中から第2引数tの文字数分の文字列がtと同じかどうかを順次調べます。アルゴリズムとしては単純で、力技っぽい仕掛けです ※3

最後に、文字列sの文字数がtより短くなれば、両者は絶対に同じにはならないため、breakでforループから抜け出して処理を終えます。
if (strlen(t) > strlen(s)) {
  break;

このプログラムに簡単なソースファイルを記述した"test.txt"を読み込ませ、その中から"printf"という文字列を探させた結果は画面1のようになります。


この手法は無駄が多いため、文字列検索処理としての効率はよくありません。もっと効率的な仕組みについては、アルゴリズムを解説するときに紹介する予定です
リスト3 : 文字列から文字列を探し出すプログラムのソース[ex3502.c
#include <stdio.h>
#include <string.h>

int count = 0;

/* ---------------------------------------------
    search -- sからtを探し出現位置を返す
    見つけた数(広域変数 count )を加算する
   --------------------------------------------- */
int search(const char *s, const char *t)
{
  int i;

  /* sの文字数分繰り返す */
  for (i=0; i<strlen(s); i++) {
    if (strncmp(s, t, strlen(t)) == 0) {
      /* tの含まれる行を出力 */
      printf("¥t%s in %s¥n", t, s);
      count++;  /* 見つけた数の累計を加算 */
      /* 先頭からの字数なのでiを1増加 */
      return (i+1);
    }
    /* 1文字進める */
    s++;
    /* sがtより短くなれば終了 */
    if (strlen(t) > strlen(s)) {
      break;
    }
  }
  /* 見つからなければ-1を返す */
  return (-1);
}

/* ---------------------------------------------
   main
     標準入力から第1パラメータの文字列を探す
   --------------------------------------------- */
int main(int argc, char *argv[])
{
  char buf[1024 + 1];  /* 1行分の保存場所 */
  int line = 1;        /* 行番号カウンタ */
  int clmn;            /* 桁位置を保持 */

  /* 標準入力から1行ずつ受け取る */
  while (gets(buf)) {
    clmn = search(buf, argv[1]);

    /* 文字列が見つかったとき */
    if (clmn >= 1) {
      printf("%04d : Line %04d / Col %04d¥n",
               count, line, clmn);
    }
    line++;    /* 行番号を1増加 */
  }
  /* 広域変数から見つかった文字列の累計を表示 */
  printf("--------¥nTotal : %d word(s) found.¥n", count);
}

広域変数の役割

リスト3の冒頭で宣言しているint型の変数countが広域変数です。宣言と同時に0で初期化しています。
int count = 0;

このcountはソース中の2つの関数searchとmainの両方から参照できます。関数searchでは、文字列sから文字列tが見つかった際に、見つかった文字列の数の累計を記録するため、以下のようにインクリメントされます。
count++;  /* 見つけた数の累計を加算 */

一方main関数では、search関数を呼び出して文字列が見つかったとき、その結果を表示するprintf関数の引数として参照しています。
if (clmn >= 1) {
  printf("%04d : Line %04d / Col %04d¥n",
           count, line, clmn);
}

マルチモジュールで作る

同じプログラムのソースを、main関数とsearch関数の2つのソースファイルに分けて記述し、マルチモジュールとしてみましょう。

main関数を定義したソースがリスト4、search関数を定義したソースがリスト5です。ファイル名をそれぞれex3503.cとex3504.cとしています。

これら2つの.cソースファイルをそれぞれコンパイルすると、ex3503.objとex3504.objの2つの中間形式ファイルが生成されます。それらとライブラリなどをリンクして出来上がった実行形式ファイルは、先に紹介したリスト3のex3502.cをコンパイルして出来上がったものと同じ動作をします。

リスト4 : 文字列を探し出すプログラムのマルチモジュール版(main関数部分)
[ex3503.c(sfind.c)]
#include <stdio.h>

extern int search(const char *s, const char *t);
extern int count;

/* ---------------------------------------------
   main
     標準入力から第1パラメータの文字列を探す
   --------------------------------------------- */
int main(int argc, char *argv[])
{
  char buf[1024 + 1];  /* 1行分の保存場所 */
  int line = 1;        /* 行番号カウンタ */
  int clmn;            /* 桁位置を保持 */

  /* 標準入力から1行ずつ受け取る */
  while (gets(buf)) {
    clmn = search(buf, argv[1]);

    /* 文字列が見つかったとき */
    if (clmn >= 1) {
      printf("%04d : Line %04d / Col %04d¥n",
               count, line, clmn);
    }
    line++;    /* 行番号を1増加 */
  }
  /* 広域変数から見つかった文字列の累計を表示 */
  printf("--------¥nTotal : %d word(s) found.¥n", count);
}

リスト5 : 文字列を探し出すプログラムのマルチモジュール版(search関数部分)
[ex3504.c(search.c)]
#include <stdio.h>
#include <string.h>

/* ---------------------------------------------
    search -- sからtを探し出現位置を返す
    見つけた数(広域変数 count )を加算する
   --------------------------------------------- */

int count = 0;

int search(const char *s, const char *t)
{
  int i;

  /* sの文字数分繰り返す */
  for (i=0; i<strlen(s); i++) {
    if (strncmp(s, t, strlen(t)) == 0) {
      /* tの含まれる行を出力 */
      printf("¥t%s in %s¥n", t, s);
      count++;  /* 見つけた数の累計を加算 */
      /* 先頭からの字数なのでiを1増加 */
      return (i+1);
    }
    /* 1文字進める */
    s++;
    /* sがtより短くなれば終了 */
    if (strlen(t) > strlen(s)) {
      break;
    }
  }
  /* 見つからなければ-1を返す */
  return (-1);
}

extern宣言で外部の識別子を参照する

main関数を定義しているリスト4では、冒頭で以下のようにexternを付けて関数searchと変数countを宣言しています。
extern int search(const char *s, const char *t);
extern int count;

変数と同じように、他のソースファイルなどで定義されている関数もexternで宣言すれば外部の識別子として参照できるようになります。この場合の「参照」とは、呼び出して戻り値を得ることができる――ということです。

search関数を定義しているリスト5では、広域変数countを宣言しています。countの実体はこのソースの中に存在しており、main関数を定義しているリスト4から見れば外部広域変数となります。
int count = 0;

関数も外部参照できる

変数だけではなく関数も、extern宣言することでソースファイルの外にあるものを参照できます。

ライブラリに存在する標準関数も、それを利用するにはextern宣言が必要です。それらはstdio.hなど処理系に用意されたヘッダファイルに記述されているため、ライブラリに適合したヘッダファイルを#includeプリプロセッサ指令で取り込む必要があるのです。

【参考】
LSI-Cで複数の.cソースファイルから1つの実行形式ファイルを生成するには、以下のようなコマンドラインを入力します。
LCC <オプション> -o <実行形式ファイル名> <ソースファイル名1> <ソースファイル名2> ...

今回のサンプルでは、ex3503.cをsfind.c、ex3504.cをsearch.cというファイル名に変更し、最終的にsfind.exeを生成しました。コマンドラインは以下のようになります(すべてのソースファイルが"C:¥CLANG"フォルダに存在するという前提です)。
LCC -j -oC:¥CLANG¥sfind.exe C:¥CLANG¥sfind.c C:¥CLANG¥search.c


変数の通用範囲――参照の垣根について説明してきました。変数の通用範囲には「生成から消滅までの範囲」と「値を参照できる範囲」の2通りの見方があります。以下の点を押さえておきましょう。

・生成から消滅までの範囲(期間)
自動変数と静的変数

・値を参照できる範囲(領域)
局所変数と広域変数


・自動変数
  関数内で生成されて関数の終了とともに消滅する。

・静的変数
  関数の最初の呼び出し時に生成され、関数が終了しても値が保持される。


・局所変数
  関数内で宣言され、関数内でのみ値を参照できる

・広域変数
  関数の外で宣言され、ソースファイル内のどこからでも値を参照できる。

つまり、
関数内で宣言された自動変数は
同時に局所変数でもある
ということです。

また、広域変数には以下の2種類があります。

・内部広域変数
  ソース内で宣言された広域変数

・外部広域変数
  ソースの外で宣言された(宣言されているはずの)広域変数

ここで説明した『参照の垣根』は変数だけではなく、関数や記号定数などすべての識別子が対象となります。

【サンプルファイル実行の注意点】
ex3502.exeとsfind.exeには、以下のような問題点があります。

・お使いのOSによっては日本語の部分が文字化けする可能性がありますが、再コンパイルすることで回避できます。

・日本語の含まれたファイルから日本語を探し出す仕様とはなっておりませんので、その場合はXPでも文字化けすることがあります。

・読み込みバッファを1024バイトとしているため、1行の長さが1024バイトを超えたときの動作は不安定です。

本サンプルは、あくまで広域変数の例として作成したプログラムであるため、日本語環境での完全な動作を保証するものでないことをご了解願います。