C言語編 第41章 ランダムアクセス

先頭へ戻る

この章の概要

この章の概要です。


ランダムアクセス

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

前章で、いろいろな関数を使ってファイルの読み書きを行いましたが、その際、ファイルポジションという概念が登場しました。ファイルポジションは、読み書きを行うファイル内の位置のことで、読み書きを行うと自動的に動きます。

ファイルを開いた直後、ファイルポジションはファイルの先頭にあり、読み書きを行うことで移動していきます。これはシーケンシャルアクセスをしているということです。

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

まず、前章で登場した rewind関数があります。これはファイルの先頭にファイルポジションを戻します。なお、rewind関数は、その時点までに発生していたファイルのエラーについても無かったことにします。つまり、ferror関数が 0 を返す状態に戻します。

より自由度の高いシークを行えるのが、fseek関数です。fseek関数は、<stdio.h> に次のように宣言されています。

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

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

fseek関数は、rewind関数より多くのことができるものの、制限も多くあります。また、ファイルを、テキストファイルとして開いているのか、バイナリファイルとして開いているのかによっても制限事項は異なっており、少々複雑です。バイナリファイルの場合については、第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関数は、現在のファイルポジションを返す関数です。返される値は、バイナリファイルの場合には、ファイルの先頭からのバイト数であると決まっていますが、テキストファイルの場合には、どんな数値であるか環境によって異なります。そのため、テキストファイルに対する ftell関数が返した値は、先ほどの fseek関数の4番目の使い方以外には用いられません。

上記の4つの内容をよく観察すると、結局のところ、あまり自由に動き回れないことが分かります。4番目の使い方がやや難しそうですが、これは要するに、現在の位置を ftell関数に問い合わせて、その戻り値を変数に保存しておけば、その後、読み書きを行うなり、先頭や末尾へ移動するなりした後、再び元の位置に戻ってくることができるということです。

使用例を挙げます。

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

static void putFileLine(FILE* fp);

int main(void)
{
    FILE* fp;
    long pos;

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

    putFileLine( fp );            /* 1行目 */
    pos = ftell( fp );            /* ファイルポジションを保存 */
    putFileLine( fp );            /* 2行目 */
    putFileLine( fp );            /* 3行目 */
    fseek( fp, pos, SEEK_SET );   /* 保存しておいた位置へ復帰 */
    putFileLine( fp );            /* 2行目 */

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

    return 0;
}

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

    fgets( buf, sizeof(buf), fp );
    fputs( buf, stdout );
}

入力ファイル(test.txt)

aaaaa
bbbbb
ccccc

実行結果(標準出力)






fgetpos関数と fsetpos関数

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

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

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

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

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

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

fpos_t型がどのような型であるかはコンパイラによって異なりますが、通常 long int型よりも大きく定義されており、必要十分な大きさがあると考えられます。

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

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

static void putFileLine(FILE* fp);

int main(void)
{
    FILE* fp;
    fpos_t pos;

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

    putFileLine( fp );     /* 1行目 */
    fgetpos( fp, &pos );   /* ファイルポジションを保存 */
    putFileLine( fp );     /* 2行目 */
    putFileLine( fp );     /* 3行目 */
    fsetpos( fp, &pos );   /* 保存しておいた位置へ復帰 */
    putFileLine( fp );     /* 2行目 */

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

    return 0;
}

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

    fgets( buf, sizeof(buf), fp );
    fputs( buf, stdout );
}

入力ファイル(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;

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

    fseek( fp, 0L, SEEK_END );

    fputs( "xyz", fp );

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

    return 0;
}

実行前のファイル(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;

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

    fputs( "xyz", fp );

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

    return 0;
}

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

aaa
bbb
ccc

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

aaa
bbb
ccc
xyz

実行結果(標準出力)






練習問題

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

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


解答ページはこちら

参考リンク



更新履歴

'2018/3/16 全面的に文章を見直し、修正を行った。
章のタイトルを変更(テキストファイルの読み書き② -> ランダムアクセス)
第40章から「シーク」の項を移動し、「ランダムアクセス」に修正。 fgetpos関数と fsetpos関数に関する部分は、「fgetpos関数と fsetpos関数」に分けた。
fputc関数と fgetc関数」「fprintf関数と fscanf関数」の項を、第40章へ移動。
「putc関数と getc関数」の項を削除。
練習問題を変更。 元あった①を削除、②③を第40章へ移動。 第39章にあった②を①として持ってきた。 ②を新規で作成。

'2018/2/1 C言語編全体で表記を統一するため、「フォーマット指定」を「変換指定」に改めた。

'2017/5/13 サンプルプログラムから、使われていないマクロの定義を削除。

'2017/5/11 FILE周りの用語について、表現を見直した。

'2015/8/29 flose関数の戻り値もチェックするようにした。

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



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

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

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

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー