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」をテキストエディタで開いてみると、プログラムの起動時間がスペースで区切られて書き込まれているのが確認できます。
(年、月、日、時、分、秒)
fscanf関数で読み込みたい文字列

初回起動時は、起動日時を「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つ用意して引数に渡しています。
fscanf関数で読み込みたい文字列

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にはデータは格納されない
}

現在の日時の取得

最初のサンプルコードでは現在の日付を取得して利用しています。
これに関しては日時の取得で詳しく説明します。