fscanf関数

ファイル処理4

標準入出力からキーボード入力を受け取る関数に、scanf関数というものがあります。
この関数はprintf関数と対になる関数ですが、変換指定子の指定の仕方が難しく、あまり初心者向けではない関数なのであえて説明を避けてきました。

ファイルの書き込みにfprintf関数があるように、ファイルの読み込みにはfscanf関数(fscanf_s関数)があります。
ファイルから文字列を読み込んで変数に値を格納する場合には、fgetc関数やfgets関数よりもfscanf関数を利用したほうが便利なことがあるので紹介します。

#include <stdio.h>
#include <time.h>

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);
}

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関数を利用して読み込んで表示したわけです。
(17~26行目)

fscanf(fscanf_s)関数

int fscanf(
 FILE *stream,
 const char *format [,
 argument ]...
);
ファイルストリームstreamから書式指定文字列formatに従って変数に値を受け取る。
戻り値は受け取った値の数。
int fscanf_s(
 FILE *stream,
 const char *format [,
 argument ]...
);
ファイルストリームstreamから書式指定文字列formatに従って変数に値を受け取る。
戻り値は受け取った値の数。

例によって、fscanf_s関数はfscanf関数のセキュリティ強化版です。
Visual Studioではfscanf関数を使用するとエラーになるので、fscanf_s関数を使用します。
両者は引数も戻り値も同じですが、文字列を受け取る場合は使い方が若干異なります。
(後述)

fscanf関数は、指定したファイルから文字列を読み取ります。
読み取られた文字列は、書式指定文字列に従って第三引数以降の変数に格納されます。

引数

fscanf関数の引数はfprintf関数とよく似ています。

最初の引数は読み取りたいファイルのポインタです。

第二引数の書式指定文字列はprintf関数の時とほとんど同じです。
整数として読み取りたい場合は「%d」、文字列として読み取りたい場合は「%s」を指定します。
そして、実際に格納したい変数を第三引数以降に順に指定します。

ただし、printf関数の時とは違って変数にはアドレス演算子(&記号)を付加する必要があります。
fscanf関数はポインタを利用して変数に値を保存しているわけです。
(文字列配列を指定する場合は配列名=先頭要素へのポインタなので、アドレス演算子は必要ありません)

fscanf関数は先頭から文字列を読み取っていき、半角スペース、タブ文字、改行などを区切り文字とし、それらが表われるまで文字列を読み取ります。
そして変換指定子に従って文字列を変換し、変数に格納します。
次の読み取りは前回読み取った位置から区切り文字を飛ばした次の文字から開始します。
区切り文字が連続している場合は区切り文字以外が登場するまで読み飛ばされます。

つまり上記の例では「2017」とその次の「2」は別の変数に格納されることになります。
このファイルには全部で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関数で文字列配列に値を受け取る場合、文字列配列の指定に続いて受け取る文字数を指定します。
これを超えるデータは受け取らないので、バッファオーバーラン(意図しないメモリ上のデータへの書き込み)を防ぐことができます。

ファイル書き込み側の処理

サンプルコードのファイルの書き込み処理では、現在の日付と時間を取得してテキストファイルに保存しています。
この時のテキストは、後にfscanf関数で読み取ることを想定してひとつずつ半角スペースで区切っておきます。
(47行目)

現在の日時の取得

上記コードでは現在の日付を取得して利用しています。
これに関しては日時の取得で詳しく説明します。