ランダムアクセス

ファイル処理6

ランダムアクセスとは

今までのファイル処理は、先頭から順に読み書きするものでした。
これでも一応ファイルを扱うことはできますが、巨大なファイルの場合に毎回先頭から処理をしていては効率が良くありません。
ファイル内の読み書きしたい位置が分かっているならば、ダイレクトにその場所を指定できたほうが良いでしょう。

ファイル内の位置を指定して読み書きする方法をランダムアクセスといいます。
先頭から順に処理する方法をシーケンシャルアクセスといいます。

ファイル位置(ファイル位置指示子)を移動させるにはfseek関数を使用します。


#include <stdio.h>

void Write(const char *file)
{
    FILE *fp;

    //fp = fopen(file, "wb");
    fopen_s(&fp, file, "wb");

    if (fp == NULL)
    {
        printf("%sのオープンに失敗しました。\n", file);
        return;
    }

    int intArr[] = { 1, 2, 3, 4 , 5,
                    6, 7, 8, 9, 10 };

	//ファイル書き込み
    fwrite(intArr, sizeof(int), 10, fp);

    fclose(fp);

    printf("%sに保存しました。\n", file);
}

void Read(const char *file)
{
    FILE *fp;

    //fp = fopen(file, "rb");
    fopen_s(&fp, file, "rb");

    if (fp == NULL)
    {
        printf("%sのオープンに失敗しました。\n", file);
        return;
    }

    int integer;

	//ファイル位置を指定する変数
    size_t pos;

	pos = sizeof(int) * 2;
    fseek(fp, pos, SEEK_SET);
    fread(&integer, sizeof(int), 1, fp);
    printf("先頭から%dバイト目のデータ: %d\n", pos, integer);

    pos = sizeof(int) * 0;
    fseek(fp, pos, SEEK_CUR);
    fread(&integer, sizeof(int), 1, fp);
    printf("前回から%dバイト後のデータ: %d\n", pos, integer);

    pos = sizeof(int) * 3;
    fseek(fp, pos, SEEK_CUR);
    fread(&integer, sizeof(int), 1, fp);
    printf("前回から%dバイト後のデータ: %d\n", pos, integer);

    pos = sizeof(int) * 4;
    fseek(fp, 0 - pos, SEEK_CUR);
    fread(&integer, sizeof(int), 1, fp);
    printf("前回から%dバイト前のデータ: %d\n", pos, integer);

    fclose(fp);
}

int main()
{
    const char *file = "C:\\test.dat";

    Write(file);
    Read(file);

    getchar();
}
C:¥test.datに保存しました。
先頭から8バイト目のデータ: 3
前回から0バイト後のデータ: 4
前回から12バイト後のデータ: 8
前回から16バイト前のデータ: 5

サンプルコードで作成されるバイナリファイルは1~10の整数を順番に書き込んだだけのものです。
32ビット環境ではint型は4バイトのサイズなので、先頭から0バイト目は1、4バイト目は2…という値が取得できます。
ファイル位置の指定は先頭から指定だけでなく、前回の位置から見て前や後ろに○バイトの位置、といった指定も可能です。

fseek関数

int fseek(
 FILE *stream,
 long offset,
 int origin
);
ファイルストリームstreamのファイル位置指示子をoriginを基準にoffsetバイト移動する。
戻り値は、正常終了時に0。
エラー時は0以外を返す。

fseek関数の第三引数には以下の定数を指定します。

SEEK_SET
ファイルの先頭。
SEEK_CUR
現在の位置。
SEEK_END
ファイルの終端。
バイナリモードでは指定してはならない。

読み取り側の処理では、まずfseek関数の第三引数にSEEK_SETをセットし、ファイル先頭から8バイト目のデータを読み取っています。
今回のファイルデータはint型で「1,2,3,…,10」という並びなので、先頭から8バイト目は「3」となります。

fread関数でデータを読み取ると、ファイル位置は読み取ったデータの直後にセットされます。
次のfseek関数では第三引数にSEEK_CURを指定し、位置の指定には「0」を指定しています。
前回の位置から0バイト後ろ、つまりファイル位置を動かさない状態でデータを読むと次のデータが読み取られます。
(つまり、このfseek関数は無意味ということです)

「4」を読み取った後、つまり「5」の位置から12バイト後ろのデータは「8」になります。
(12バイト ÷ 4バイト(int型サイズ) = 3個後ろのデータ)
「9」の位置から16バイト前のデータは「5」となります。
(16バイト ÷ 4バイト = 4個前のデータ)

シーケンシャルアクセスの場合、不要なデータを読み捨てることで「ファイル位置を後ろに移動させる」ことはできますが、「ファイル位置を前に戻す」ということはできません。
前の位置にあるデータが欲しい場合は先頭から読み直すしかないのですが、fseek関数を使えば「今の場所から○バイト前/後」のデータを簡単に読み取ることができます。

ただし、ファイルの構造が分からないと正しくデータを読み取ることはできません。
ファイルのどこにどのようなデータが何バイト入っているかを把握しておくか、それを知る方法を用意しておく必要があります。
(例えばどこに何バイトのデータが存在するかをファイル先頭に定義しておくなど)

バイナリモードのSEEK_ENDについて

fseek関数のファイル位置の基準の指定にSEEK_ENDがあります。
これはファイルの終端位置から相対指定をするものですが、バイナリモードで開いているファイルに対してこれは指定できません。
コンパイラによっては上手く動くかもしれませんが、C言語では定義されていない動作となるので意図しない動作となる可能性があります。

テキストモードでのfseek関数

ファイルをテキストモードで開いているとき、fseek関数のoffset(第二引数)とorigin(第三引数)には制限があり、以下のどちらかの条件に従う必要があります。

offsetは0を指定する

バイナリモードではファイル位置を調節するためにoffsetにバイト数を指定していましたが、このような調節はできません。
つまり、ファイル位置を先頭(SEEK_SET)にするか末尾(SEEK_END)にするかのどちらかの操作しかできません。
(SEEK_CURを指定しても意味がありません)

offsetはftell関数の戻り値を指定する

もう一つ、offsetにはftell関数の戻り値を指定することができます。

long ftell(
 FILE *stream
);
ファイルストリームstreamのファイル位置子を返す。

ftell関数は、現在のファイル位置を返す関数です。
この関数によって取得した値であればoffsetに指定することができます。
この場合、originには必ずSEEK_SETを指定します。


//読み込むテキストファイルには
//ABCDEFGHI...の文字列があるものとする

FILE *fp;
fopen_s(&fp, "C:\\test.txt", "r");
//エラー処理
//...

char str1[3];
char str2[3];
char str3[3];

fread(str1, sizeof(char), 2, fp);
str1[2] = '\0';

//「AB」まで読み込んだ時点のファイル位置を保存しておく
long pos = ftell(fp);

fread(str2, sizeof(char), 2, fp);
str2[2] = '\0';

//ファイル位置を戻す
fseek(fp, pos, SEEK_SET);

fread(str3, sizeof(char), 2, fp);
str3[2] = '\0';

printf("%s\n", str1); //AB
printf("%s\n", str2); //CD
printf("%s\n", str3); //CD

通常、fread関数を使用する毎にファイル位置は読み取った分だけ後ろに移動するので、取得される値はそれぞれ「AB」「CD」「EF」となるはずです。
しかし23行目のfseek関数により、ファイル位置を「AB」の読み込取り直後に戻しているため、三回目のfread関数でも「CD」が読み取られることになります。

なお、fread関数はファイルの現在位置から指定したバイト数のデータを読み取る、という関数に過ぎません。
文字列を読み取った場合、文字列配列の最後にNULL文字が付くことは保障されません。
読み取ったデータの末尾にNULL文字が存在しないことが分かっているのであれば、サンプルコードのように手動でNULL文字を付加する必要があります。

ちなみにfgets関数は文字列を取得するための関数なので、自動的に末尾にNULL文字が付加されます。

改行コードの取り扱い

Windows環境では、改行コードは実は二文字(2バイト)で表現されています。
MacOS(UNIX、Linux)環境では、改行コードは一文字です。
(古いMacOSでも一文字ですが、コードの種類が異なります)

OS 改行コード 16進数
Windows CR+LF
(\r\n)
0D 0A
Mac OS X(UNIX、Linux) LF
(\n)
0A
Mac OS 9以下 CR
(\r)
0D

例えば、「ABC(改行)DEF」という文字列をWindowsとMacOSのテキストエディタで作成するとします。
それらをバイナリデータとして表示すると以下のようになります。
WindowsとMacOSの改行コードの違い

今までのサンプルコードでは、文字列の改行はすべて「\n」を用いてきましたが、Windows環境ではファイルの読み取りの際に「\r\n」は「\n」に、書き込みの際に「\n」は「\r\n」に自動的に変換されます。
ただし変換されるのはテキストモードの場合で、バイナリモードでは変換は行われません

何が問題か

MacOSで作成したテキストファイルを、Windowsでバイナリモードで開いた時、改行コードを二文字として扱ってしまうとデータがズレてしまいます。
上記の図の例では、「D」を取得しようと先頭から6文字目を指定しても、実際に取得されるのは「E」となります。
もちろんOSが逆の場合でも問題は発生します。

「現在のファイル位置から『\n』までを一行とみなし文字列配列に保存する」というようなコードだと、Windowsで作成したテキストを読み込むと配列の最後に「\r」という不要なデータが含まれてしまいます。
さらに、MacOS9以前に作られたテキストだと改行コードを検出できません。

バイナリモードとテキストモード、さらに各OSでの改行コードの違いに注意しないと思ったように動作しない可能性があります。