fscanf関数
ファイル処理4
標準入出力からキーボード入力を受け取る関数に、scanf
関数というものがあります。
この関数はprintf
関数と対になる関数ですが、変換指定子の記法が難しく、あまり初心者向けではない関数なのであえて説明を避けてきました。
ファイルの書き込みにfprintf
関数があるように、ファイルの読み込みにはfscanf
関数(fscanf_s
関数)があります。
ファイルから文字列を読み込んで変数に値を格納する場合には、fgetc
関数やfgets
関数よりもfscanf
関数を使用したほうが便利な場面も多いので紹介します。
#include <stdio.h>
#include <time.h>
//ファイルパスfileからデータを読み取る
void Read(const char *file)
{
FILE *fp;
//fp = fopen(file, "r");
fopen_s(&fp, file, "r");
if (fp == NULL)
{
printf("初回の起動です。\n");
return;
}
//ファイルから読み取り
int year, mon, day, hour, min, sec;
fscanf_s(fp, "%d%d%d%d%d%d",
&year, &mon, &day,
&hour, &min, &sec
);
//読み取ったデータを表示
printf("前回の起動日時\n");
printf("%04d/%02d/%02d %02d:%02d:%02d",
year, mon, day,
hour, min, sec
);
fclose(fp);
}
//ファイルパスfileにデータを書き込む
void Write(const char *file)
{
FILE *fp;
time_t t;
//現在の日時を取得する処理
time(&t);
struct tm local;
localtime_s(&local, &t);
//fp = fopen(file, "w");
fopen_s(&fp, file, "w");
if (fp == NULL)
{
printf("%sのオープンに失敗しました。\n", file);
return;
}
//ファイルにデータを書き込む
fprintf(fp, "%d %d %d %d %d %d",
local.tm_year + 1900, local.tm_mon + 1, local.tm_mday,
local.tm_hour, local.tm_min, local.tm_sec
);
fclose(fp);
}
int main()
{
const char *file = "C:\\test.txt";
Read(file);
Write(file);
getchar();
}
このコードは「C:\test.txt」に新規にファイルを作成します。
「C:\test.txt」にファイルが存在する場合、このコードの実行前に削除しておいてください。
初めてコードを実行すると、「初回の起動です。」と表示されます。
一度プログラムを終了して再度実行すると、前回の起動日時が表示されます。
「C:\test.txt」をテキストエディタで開いてみると、プログラムの起動時間がスペースで区切られて書き込まれているのが確認できます。
(年、月、日、時、分、秒)
初回起動時は、起動日時を「C:\test.txt」に書き込む処理だけを行います。
次回の起動時にはこの文字列をfscanf
関数を利用して読み込んで表示したわけです。
printf
関数の%04d
は数値を4桁表示する変換指定子です。
%02d
は2桁表示の指定です。
fscanf(fscanf_s)関数
-
int fscanf(
FILE *stream,
const char *format [,
argument ]...
); -
ファイルストリームstreamから書式指定文字列formatに従って変数に値を受け取る。
戻り値は受け取った値の数。
エラーが発生した場合はEOFを返す。
-
int fscanf_s(
FILE *stream,
const char *format [,
argument ]...
); -
ファイルストリームstreamから書式指定文字列formatに従って変数に値を受け取る。
戻り値は受け取った値の数。
エラーが発生した場合はEOFを返す。
fscanf_s
関数はfscanf
関数のセキュリティ強化版です。
Visual Studioの初期設定ではfscanf
関数を使用するとエラーになるのでfscanf_s
関数を使用します。
引数宣言や戻り値など、基本的な使い方は同じですが、文字列を受け取る場合は引数の指定方法が異なります。
(後述)
fscanf
関数は、指定したファイルから文字列を読み取ります。
読み取られた文字列は、書式指定文字列に従ってデータ型が変換され、第三引数以降の変数に格納されます。
最初の変換が行われる前にファイル終端に達した場合はエラーとなり、EOFを返します。
引数
fscanf
関数の引数はfprintf
関数とよく似ています。
最初の引数は読み取りたいファイルへのポインタです。
第二引数の書式指定文字列は変換指定子の指定です。
ファイルからは文字列を読み取りますが、この変換指定子に従ってデータ型が変換されます。
変換指定子はprintf
関数の時とほとんど同じで、整数として読み取りたい場合は%d
、文字列として読み取りたい場合は%s
を指定します。
第三引数以降は書式指定文字列で指定した通りに変数を順に指定します。
ただし、printf
関数の時とは違って変数にはアドレス演算子(&
記号)を付けて指定する必要があります。
つまりfscanf
関数内ではポインタを通して変数の値の書き換えを行っています。
なお、文字列配列を指定する場合は配列名=先頭要素へのポインタなので、アドレス演算子は必要ありません。
fscanf
関数は、半角スペース、タブ文字、改行などを区切り文字とし、それらが現れるまで文字列を読み取ります。
読み取った文字列を変換指定子に従って変換し、変数に格納します。
次の読み取りは前回読み取った位置から区切り文字を飛ばした次の文字から開始します。
区切り文字が連続している場合は区切り文字以外が登場するまで読み飛ばされます。
以下の例では「2017」とその次の「2」は別の変数に格納されることになります。
このファイルには全部で6つの数値が書き込まれているので、変換指定子を6つ指定し、変数も6つ用意して引数に渡しています。
テキストファイルをfgets
関数などで読み取ると、データはすべて文字(文字列)として読み取られます。
数値として扱いたい場合は読み取り後にatoi関数などで変換しなければなりません。
fscanf
関数を利用すると、読み取りと変換を同時に行うことができます。
fscanf関数の注意点
fscanf
関数は便利な関数ですが、読み取り先のファイルに書式指定文字列で指定した通りの文字列が存在しないと、意図した通りに値を受け取ることができません。
これはscanf
関数も同様で、特にscanf
関数の場合はユーザーがどのようなキー入力をするかはわかりません。
意図した通りの入力をしてくれれば良いのですが、想定外の入力があるとデータがおかしくなり、最悪の場合はプログラムの異常終了や脆弱性になる可能性があります。
ユーザーがどのような値を入力しても問題が発生しないようにするには、scanf
関数の書式指定文字列の書き方をかなり工夫せねばなりません。
初心者にはこれが難しいので、scanf
関数の使用はあまりお勧めできません。
(もちろんきちんと書けるならば使用しても問題ありません)
fscanf
関数も同じ問題がありますが、今回のサンプルコードのようにプログラム側で決まりきった形式のファイルを出力し、それを読み込むのであれば使用しても問題ありません。
ただし、ファイルの破損や改ざんなどで意図しないデータとなっている可能性はあるので、本格的なプログラムで使用する場合はしっかりとしたエラー処理は必要です。
fscanf関数とfscanf_s関数の違い
fscanf
関数とfscanf_s
関数は、文字列を受け取る場合に引数の指定の仕方が異なります。
char str[10];
//fscanf関数
fscanf(fp, "%s", str);
//fscanf_s関数
fscanf_s(fp, "%s", str, 10);
fscanf_s
関数で文字列配列に値を受け取る場合、文字列配列の指定に続いて受け取るバイト数(配列の要素数)を指定します。
これを超えるデータは受け取らないので、バッファオーバーラン(意図しないメモリ上のデータへの書き込み)を防ぐことができます。
配列サイズは終端のNULL文字を含めて格納可能である必要があります。
ファイルから読み取った文字列サイズが配列サイズを超える場合、変数への格納は失敗し、以降の読み取りは中止されます。
すべてを正常に読み取れたか否かは戻り値でチェックできます。
FILE* fp;
int n1, n2;
char str[8];
//数値、文字列(長さ8),数値、の3つを読み取る
int r = fscanf_s(fp, "%d%s%d",
&n1, str, 8, &n2);
//戻り値が3未満ならどこかでエラー
if (r < 3) {
//仮に文字列の読み取り箇所で失敗していた場合、
//配列strおよびn2にはデータは格納されない
}
現在の日時の取得
最初のサンプルコードでは現在の日付を取得して利用しています。
これに関しては日時の取得で詳しく説明します。