データの読み込み

連続的にデータを読み込み、意味のある文字列や数値を取り込んでいく 方法を実例とともにおぼえよう。

入力の復習

キーボードから何かを入力するときの基本的な手順は、

  1. 読み込みに必要なバッファ(char型配列)を用意する
  2. fgets関数でstdinから読み込む
  3. バッファにある文字列をatoiで数値化したりする

となる。より複雑なデータを読む場合も基本的な手順は同じである。

複合データの読み込み。

たとえば、一行の中に複数の項目が含まれるデータを読むことを考えよう。 以下のような成績データがあったとしよう。

山田太郎	50
公益太郎	90
飯森花子	91
鶴岡一人	60
酒田三吉	52
三川一二三	12

このデータを読み込み、全員の平均点を求めて 表示するようなプログラムを作ってみよう。

連続的なデータの読み込み

平均点を計算するためには、まず全員のデータを読み込んで、得点と人数を 求める必要がある。

最初に、データファイルを全部読み、合計点を求めよう。 データを全部読むループは次のように書く。

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

int main()
{
  char buffer[100];
    :
  while (NULL != fgets(buffer, sizeof buffer, stdin)) {
    :
  }
}

fgets 関数は、読み込みに成功した場合は読み込んだデータの 入っているアドレス(つまりbuffer)を返す。データの終了に 達した場合は特別なコード NULL を返す。したがって、 データがある限り読み続けたい場合は、fgets の値が NULL でないことを確認する条件式を while に 与えてループを構成すると良い。

データを読み込んで行番号を付けて表示するだけのプログラム disp.c は以下のようになる。

#include <stdio.h>

int main()
{
  char buffer[100];
  int i=1;
  while (NULL != fgets(buffer, sizeof buffer, stdin)) {
    printf("%3d: %s", i++, buffer);
  }
}

実際にコンパイルして実行してみよう。

% gcc -o disp disp.c
% ./disp
やーやー
  1: やーやー
はろはろー
  2: はろはろー
[C-d]

キーボードからデータを入力する場合、データの最後を示すときには 行の先頭で C-d をタイプする。

データをその都度いちいち入れたのではたいへんなので、普通は データとなる内容をファイルに保存しておいて、これをCで作成したプログラム に流し込むようにする。fgets の第3引数に指定した stdin は、標準入力(STandard INput) という意味で、 これはプログラムを直接起動すると、キーボードから、以下のようにパイプライ ンの途中に起動すると、直前のプログラムが出力したものを入力として 受け取る。

% cat disp.c | ./disp
  1: #include <stdio.h>
  2: 
  3: int main()
  4: {
  5:   char buffer[100];
  6:   int i=1;
  7:   while (NULL != fgets(buffer, sizeof buffer, stdin)) {
  8:     printf("%3d: %s", i++, buffer);
  9:   }
 10: }

確認のため、cat disp.c だけで起動した場合の 結果も見ておくこと。

cat disp.c の代わりに、

などのコマンドについて

  1. そのコマンドだけで起動
  2. コマンドの後にパイプラインで繋げて ./disp を起動

の両方について実行してみよ。

読んだデータの分解

実際の試験データの1行分、

山田太郎	50

これを、文字列と数値に分解する方法にはいくつかあるが、 項目が空白で区切られているという前提のときに、最も手軽に利用できるのが sscanf 関数である。

sscanf の利用例を挙げてみよう。元の文字列が変数 buffer に入っているとしよう。内容は以下のとおり。

山田太郎	50

これは、

文字列 空白(の連続) 数値(整数)

という並びである。これを解析するフォーマットはprintfの ものとほぼ同じであり、sscanfで、以下のようにする。

sscanf(buffer, "%s %d", 文字列をしまうアドレス, 整数をしまうアドレス)

まず、「文字列をしまう」変数、と「整数をしまう」変数を用意しよう。

char name[50];
int point;

このように宣言したならば、sscanfは以下のようにする。

sscanf(buffer, "%s %d", name, &point);

ここで変数のアドレスを指定する方法 を思い出しておこう。

sscanf に与える第2引数は以下の意味を持つ。

ただし、入力したデータから文字列を読み取るときは、 宣言したchar配列より長いものが入力されるとプログラムが破壊される ことがある。これを防止するには%sに最大限の長さを指定し、

sscanf(buffer, "%49s %d", name, &point);

とするのが安全である。これで char name[50] には 最大49文字分の文字列と\0が納まることが保証される(はみ出た部分は切り捨て られる)。

これをまとめて、連続する成績データを解析して、名前と得点を抜き出す プログラムは以下のように書ける。

listup.c

#include <stdio.h>

int main()
{
  /* 1行は100バイト、氏名は50バイトあればいいだろう */
  char buffer[100], name[50];
  int point;

  while (NULL != fgets(buffer, sizeof buffer, stdin)) {
    sscanf(buffer, "%49s %d", name, &point);
    printf("%s さんは %d 点でした\n", name, point);
  }
  puts("以上");
}

データファイル(score.txt)を与えてこのプログラムを実行す ると以下のようになる。

% cat score.txt | ./listup
山田太郎 さんは 50 点でした
公益太郎 さんは 90 点でした
飯森花子 さんは 91 点でした
鶴岡一人 さんは 60 点でした
酒田三吉 さんは 52 点でした
三川一二三 さんは 12 点でした
以上

その他のデータ型の読み込み

文字列と整数(int)以外のデータをsscanfで読み込む場合の フォーマット指定についてもまとめておく。

入力データの正しさの確認

sscanfでは、入力した行に必要な情報が決められた順番で書か れていることを仮定している。実際にはおかしな入力行が来る場合もある。 これを調べるには、sscanfが返す値(返却値)を確認する。 sscanfは、実際に書式指定子で取り込めた値の箇数を返す。 たとえば、

   sscanf(buffer, "%49s %d", name, &point);

とした場合、sscanf の第3引数以後に指定した変数(のアドレス) は2個である。両方が正しく読み込めた場合は 2を返すが、入力データが中途半端で name しか代入できなかった 場合は sscanf 自体の値は1となる。

これを必ず調べることで、異常データがあった場合に変な値を 取り込まずに済む。以上をふまえてプログラムを修正すると 以下のようになる。

listup2.c

#include <stdio.h>

int main()
{
  /* 1行は100バイト、氏名は50バイトあればいいだろう */
  char buffer[100], name[50];
  int point;

  while (NULL != fgets(buffer, sizeof buffer, stdin)) {
    if (2 == sscanf(buffer, "%49s %d", name, &point)) {
      printf("%s さんは %d 点でした\n", name, point);
    }
  }
  puts("以上");
}

実際に、変なデータをまぜて実行してみよう。

bad-score.txt

山田太郎	50
公益太郎	90 20
飯森花子
鶴岡一人	60
酒田三吉	52
三川一二三	12

このデータをプログラムに与えて実行する。

% cat bad-score.txt | ./listup
  (飯森花子さんも表示してしまう)
% cat bad-score.txt | ./listup2
  (飯森花子さんは表示しない)

scanfについて

あらかじめ形式が決められた入力データから、文字列や数値を切り出すとき にはfgets+sscanfを使うのが基本である。

ただし、初心者用のC言語の参考書では、標準入力から書式つき読み込み を行なうのにscanf関数を利用するように説明していることが多い。 scanfは以下のように使う。

  char name[50];
  int  x, y;
  while (EOF != scanf("%s %d %d", name, &x, &y)) {
    /* nameに文字列、xとyに整数が入る */
    /* データ処理 がこのループに入る */
  }

一見、記述量が少なくて簡単に見えるが、scanf 関数は 入力データが期待とおりの書式でない場合にプログラムが暴走する 可能性を持っているので、実用的なプログラムでは使わない。

よって、この授業では scanf 関数は利用しないことを 約束とするが、試験(資格試験や期末試験)などには必ず登場するので 少なくとも読んで理解できるようにしておくこと。


目次