ランダムアクセス | Programming Place Plus C言語編 第41章

トップページC言語編

このページの概要

以下は目次です。


ランダムアクセス

データを、先頭から順番に順序どおりに読み書きしていくようなアクセス方法のことを、シーケンシャルアクセス、順次アクセス (sequential access) と呼びます。これに対して、任意の位置に自由に移動しながら読み書きを行う方法を、ランダムアクセス、直接アクセス (random access) と呼びます。

前章で、いろいろな関数を使ってファイルの読み書きを行いましたが、その際にファイルポジションという概念が登場しました。ファイルポジションは、読み書きを行うファイル内の位置のことで、読み書きを行うと自動的に動きます。ファイルを開いた直後、ファイルポジションはファイルの先頭にあり、読み書きを行うことで移動していきます。これはシーケンシャルアクセスをしているということです。

読み書きを行う以外にも、ファイルポジションを移動させる方法があります。このような操作を一般に、シーク (seek) と呼び、シークを行うための標準ライブラリ関数がいくつかあります。シークを行うことで、ランダムアクセスが実現できます。

シークを行うために用いる代表的な関数が、第40章でも登場した fseek関数です。fseek関数は、<stdio.h> に次のように宣言されています。

int fseek(FILE* stream, long int offset, int origin);

第1引数は FILEオブジェクトへのポインタ、第2引数にファイルポジションの移動量をバイト単位で指定します。第3引数は、シークの基準となる原点位置を指定します(詳細は後述します)。戻り値は、成功すれば 0 を、失敗すると 0以外の値を返します。

fseek関数には制限が多くあります。また、テキストファイルとして開いているのか、バイナリファイルとして開いているのかによっても制限事項は異なっており、少々複雑です。バイナリファイルの場合については、第42章であらためて説明するとして、ここではテキストファイルの場合に限った話をします。

テキストファイルの場合にできることは、以下の4つのいずれかです。

これ以外のこともできる可能性はありますが、C言語の規格上は保証されません。

  1. 第3引数を SEEK_SET にし、第2引数に 0L を指定⇒ファイルの先頭へ移動
  2. 第3引数を SEEK_CUR にし、第2引数に 0L を指定⇒現在位置のまま
  3. 第3引数を SEEK_END にし、第2引数に 0L を指定⇒ファイルの末尾へ移動
  4. 第3引数を SEEK_SET にし、第2引数に ftell関数の戻り値を指定⇒ftell関数が返した位置へ移動

第3引数に指定する SEEK_SETSEEK_CURSEEK_END はそれぞれ、stdio.h で定義されているオブジェクト形式マクロです。それぞれ、「ファイルの先頭」「現在のファイルポジション」「ファイルの末尾」を意味しています。第3引数に指定できるのは、これら3つのいずれかだけです。

また、ftell関数は、現在のファイルポジションを返す関数です。<stdio.h> に次のように宣言されています。

long int ftell(FILE* stream);

ftell関数の戻り値は、バイナリファイルの場合には、ファイルの先頭からのバイト数であると決まっていますが、テキストファイルの場合には、どんな数値であるか未規定です。テキストファイルに対する ftell関数が返した値は、先ほどの fseek関数の4番目の使い方以外には用いられません。なお、失敗した場合は、-1L が返されます。

さきほどの4項目をよく観察すると、テキストファイルにおけるファイルポジションの移動は、あまり自由度がないことが分かります。4番目の使い方は、現在の位置を ftell関数に問い合わせて、その戻り値を変数に保存しておけば、その後、読み書きを行うなり、先頭や末尾へ移動するなりした後、再び元の位置に戻ってくることができるということです。

使用例を挙げます。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void put_file_line(FILE* fp);

int main(void)
{
    FILE* fp = fopen("test.txt", "r");
    if (fp == NULL) {
        fputs("ファイルオープンに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    put_file_line(fp);            // 1行目
    long pos = ftell(fp);       // ファイルポジションを保存
    if (pos == -1L) {
        fputs("ファイルポジションの取得に失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
    put_file_line(fp);            // 2行目
    put_file_line(fp);            // 3行目
    if (fseek(fp, pos, SEEK_SET) != 0) {   // 保存しておいた位置へ復帰
        fputs("ファイルポジションの移動に失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
    put_file_line(fp);            // 2行目

    if (fclose(fp) == EOF) {
        fputs("ファイルクローズに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
}

/*
    ファイルから1行読み取って、標準出力へ出力。
    引数
        fp:     FILEオブジェクトへのポインタ。
*/
static void put_file_line(FILE* fp)
{
    char buf[80];

    if (fgets(buf, sizeof(buf), fp) == NULL) {
        fputs("ファイルからの読み込みに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    // 末尾の改行文字を取り除く
    char* p = strchr(buf, '\n');
    if (p != NULL) {
        *p = '\0';
    }

    puts(buf);
}

入力ファイル(test.txt)

aaaaa
bbbbb
ccccc

実行結果(標準出力)

aaaaa
bbbbb
ccccc
bbbbb


fgetpos関数と fsetpos関数

fseek関数や ftell関数の引数や戻り値の型は long int型です。long型が 32ビットの環境であれば、この最大値は 2,147,483,647 です(LONG_MAXマクロの置換結果)。これは、ファイルサイズとして考えると、約2GB ということです。

2GB というと、たとえば DVD1枚のデータ量の半分以下ということですから、万が一、巨大なデータを扱うことがあるとすれば、少々心もとない大きさかもしれません。

より巨大なファイルを扱うには、fgetpos関数fsetpos関数が利用できます。これらの関数は、<stdio.h> に以下のように宣言されています。

int fgetpos(FILE* restrict fp, fpos_t* restrict pos);
int fsetpos(FILE* fp, const fpos_t* pos);

restrict については、第57章で取り上げます。動作に影響はないので、今は無視して問題ありません。

fgetpos関数は、第2引数に fpos_t型のポインタを指定します。ファイルポジションはこのポインタを通して格納されます。戻り値は、成功したときに 0、失敗したとき 0以外です。

fsetpos関数は、第2引数にやはり fpos_t型の constポインタを指定します。このポインタが指す先の値を、ファイルポジションに設定します。ここで指定できる値は、fgetpos関数から得た値でなければなりません。戻り値は、成功したときに 0、失敗したとき 0以外です。

fpos_t型がどのような型であるかは処理系定義ですが、通常 long int型よりも大きく定義されており、必要十分な大きさがあると考えられます。

前の項の fseek関数と ftell関数のサンプルプログラムを、fgetpos関数と fsetpos関数で書き換えてみます。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void put_file_line(FILE* fp);

int main(void)
{
    FILE* fp = fopen("test.txt", "r");
    if (fp == NULL) {
        fputs("ファイルオープンに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    fpos_t pos;
    put_file_line(fp);     // 1行目
    if (fgetpos(fp, &pos) != 0) {   // ファイルポジションを保存
        fputs("ファイルポジションの取得に失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
    put_file_line(fp);     // 2行目
    put_file_line(fp);     // 3行目
    if (fsetpos(fp, &pos) != 0) {   // 保存しておいた位置へ復帰
        fputs("ファイルポジションの移動に失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
    put_file_line(fp);     // 2行目

    if (fclose(fp) == EOF) {
        fputs("ファイルクローズに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
}

/*
    ファイルから1行読み取って、標準出力へ出力。
    引数
        fp:     FILEオブジェクトへのポインタ。
*/
static void put_file_line(FILE* fp)
{
    char buf[80];

    if (fgets(buf, sizeof(buf), fp) == NULL) {
        fputs("ファイルからの読み込みに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    // 末尾の改行文字を取り除く
    char* p = strchr(buf, '\n');
    if (p != NULL) {
        *p = '\0';
    }

    puts(buf);
}

入力ファイル(test.txt)

1行目
2行目
3行目
4行目
5行目

実行結果(標準出力)

1行目
2行目
3行目
2行目

追記

ここでは、追記をやってみましょう。これはつまり、既存のファイルの末尾に文字を書き足すような処理です。

オープンモードを “w” や “w+” にすると、ファイルを開いた時点で中身を失ってしまうので、追記できません。“r” は読み取り専用なので、書き込むことができません。

1つの方法は、“r+” で開いて、fseek関数を使ってファイルの終わりまで移動し、書き込みを行うことです。たとえば、次のようになります。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE* fp = fopen("test.txt", "r+");
    if (fp == NULL) {
        fputs("ファイルオープンに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    if (fseek(fp, 0L, SEEK_END) != 0) {
        fputs("ファイルポジションの移動に失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    if (fputs("xyz", fp) == EOF) {
        fputs("ファイルへの書き込みに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    if (fclose(fp) == EOF) {
        fputs("ファイルクローズに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
}

実行前のファイル(test.txt)

aaa
bbb
ccc

実行後のファイル(test.txt)

aaa
bbb
ccc
xyz

実行結果(標準出力)

“r+” というオープンモードは、読み取りを行った後、書き込みも行えるというような意味合いです。まずは読み取りということなので、“r” と同様に、ファイルが存在しない場合には失敗します。

これでもいいのですが、“a” というオープンモードを使えば、より直接的に追記が実現できます。“a” は基本的に “w” と同じく書き込み専用のオープンモードですが、ファイルの中身を失いません。また、ファイルが開かれた時点で、ファイルポジションがファイルの終わりにあります。

なお、追記しつつも、読み取りも行いたいのであれば、“a+” を使います。

ややこしいので、各オープンモードの動作を表にまとめておきます。

操作 r w a r+ w+ a+
読み取り できる できない できない できる できる できる
書き込み できない できる できる できる できる できる
開くとファイルの中身は… そのまま 失われる そのまま そのまま 失われる そのまま
開くとファイルポジションは… 先頭にある 先頭にある 終わりにある 先頭にある 先頭にある 終わりにある
ファイルが存在しないときに開こうとすると… 失敗する 空のファイルが作られる 空のファイルが作られる 失敗する 空のファイルが作られる 空のファイルが作られる

このように、同じものがないことが分かります。なお、オープンモードに、バイナリファイルとして開くことを表す “b” が付いていても、これらの動作には変わりはありません。

さて、先ほどのサンプルプログラムを、“a” を使って書き換えてみます。今度は fseek関数が必要ありません。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE* fp = fopen("test.txt", "a");
    if (fp == NULL) {
        fputs("ファイルオープンに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    if (fputs("xyz", fp) == EOF) {
        fputs("ファイルへの書き込みに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    if (fclose(fp) == EOF) {
        fputs("ファイルクローズに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
}

実行前のファイル(test.txt)

aaa
bbb
ccc

実行後のファイル(test.txt)

aaa
bbb
ccc
xyz

実行結果(標準出力)


練習問題

問題① オープンモードが “w” の場合と、“a” の場合との挙動の違いを確かめてください。

問題② オープンモードの “a+” を使い、ファイルへの追記を行った後、ファイル全体を標準出力へ出力するプログラムを作成してください。


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



前の章へ (第40章 テキストファイルの読み書き)

次の章へ (第42章 バイナリファイルの読み書き)

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る