連続的にデータを読み込み、意味のある文字列や数値を取り込んでいく 方法を実例とともにおぼえよう。
キーボードから何かを入力するときの基本的な手順は、
fgets
関数でstdin
から読み込む
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
の代わりに、
cal
ls -l
ls
などのコマンドについて
./disp
を起動
の両方について実行してみよ。
実際の試験データの1行分、
山田太郎 50
これを、文字列と数値に分解する方法にはいくつかあるが、
項目が空白で区切られているという前提のときに、最も手軽に利用できるのが
sscanf
関数である。
sscanf
文字列の分解と変数への代入
#include <stdio.h> int sscanf(const char *str, const char *format, ...);
という書式で、文字列 str
にある文字列を、
与えられたフォーマット(format
)をもとに解析して
値を取りだす。取り出した値は、第3引数以後に指定したアドレスにある
変数に格納する。
sscanf
の利用例を挙げてみよう。元の文字列が変数
buffer
に入っているとしよう。内容は以下のとおり。
山田太郎 50
これは、
文字列 空白(の連続) 数値(整数)
という並びである。これを解析するフォーマットはprintf
の
ものとほぼ同じであり、sscanf
で、以下のようにする。
sscanf(buffer, "%s %d", 文字列をしまうアドレス, 整数をしまうアドレス)
まず、「文字列をしまう」変数、と「整数をしまう」変数を用意しよう。
char name[50]; int point;
このように宣言したならば、sscanf
は以下のようにする。
sscanf(buffer, "%s %d", name, &point);
ここで変数のアドレスを指定する方法 を思い出しておこう。
sscanf
に与える第2引数は以下の意味を持つ。
%s
空白以外のものが続く文字列
1個以上の空白文字(スペース・タブ)が続く
%d
int
型に変換できる整数
ただし、入力したデータから文字列を読み取るときは、
宣言したchar配列より長いものが入力されるとプログラムが破壊される
ことがある。これを防止するには%s
に最大限の長さを指定し、
sscanf(buffer, "%49s %d", name, &point);
とするのが安全である。これで char name[50]
には
最大49文字分の文字列と\0が納まることが保証される(はみ出た部分は切り捨て
られる)。
これをまとめて、連続する成績データを解析して、名前と得点を抜き出す プログラムは以下のように書ける。
#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
で読み込む場合の
フォーマット指定についてもまとめておく。
%d
整数だと見なしてint型変数に値を取り込む
%ld
(エルディー)
整数だと見なしてlong型変数に値を取り込む
%f
浮動小数点数だと見なしてfloat型変数に値を取り込む
%lf
(エルエフ)
浮動小数点数だと見なしてdouble型変数に値を取り込む
sscanf
では、入力した行に必要な情報が決められた順番で書か
れていることを仮定している。実際にはおかしな入力行が来る場合もある。
これを調べるには、sscanf
が返す値(返却値)を確認する。
sscanf
は、実際に書式指定子で取り込めた値の箇数を返す。
たとえば、
sscanf(buffer, "%49s %d", name, &point);
とした場合、sscanf
の第3引数以後に指定した変数(のアドレス)
は2個である。両方が正しく読み込めた場合は
2を返すが、入力データが中途半端で name
しか代入できなかった
場合は sscanf
自体の値は1となる。
これを必ず調べることで、異常データがあった場合に変な値を 取り込まずに済む。以上をふまえてプログラムを修正すると 以下のようになる。
#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("以上");
}
実際に、変なデータをまぜて実行してみよう。
山田太郎 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
関数は利用しないことを
約束とするが、試験(資格試験や期末試験)などには必ず登場するので
少なくとも読んで理解できるようにしておくこと。