ディスク上のファイルを先頭から順に読むだけでは、目的のデータにたどり着くまで時間がかかることがあります。
そこで、読み書き位置を任意の場所へ瞬時に移動できるのがfseekとftellです。
この2つを使えば、固定長レコードのランダムアクセスや末尾からの逆読みなどが簡単に実現できます。
C言語初心者の方にもわかりやすく、基本から実用まで順に解説します。
ファイル位置をランダムアクセス
ランダムアクセスとは
ランダムアクセスとは、ファイル内の読み書き位置を任意のバイトオフセットへ直接移動し、その場所から入出力を行う方法です。
順次アクセスでは先頭から順番に処理しますが、ランダムアクセスでは目的の位置にジャンプできるため、固定長レコードのN番目だけ読む、末尾から数バイトだけ検査するなどの用途に向いています。
C標準ライブラリではfseekで移動し、ftellで現在位置を取得します。
ポイントとして、テキストモードの改行変換がある環境(主にWindows)では、ランダムアクセスの整合性が崩れる可能性があります。
このため、ランダムアクセスは基本的に"rb"や"r+b"などのバイナリモードで行うのが安全です。
fseekとftellの基本の流れ
基本的な流れは次の通りです。
位置を変えるのがfseek、位置を知るのがftellです。
移動後はfreadやfwrite、fgetcやfputcなどで入出力を行います。
- ファイルを開く(ランダムアクセスは
"rb"や"r+b"推奨)。 fseekで移動する。ftellで現在位置を必要に応じて確認する。- 読み書きを行う。
- 戻り値を確認し、
perrorでエラー内容を表示する。
短い例(流れのイメージ)
#include <stdio.h>
int main(void) {
// バイナリ更新モードで開く(読み書き両用)
FILE *fp = fopen("demo.bin", "wb+");
if (!fp) { perror("fopen"); return 1; }
// 既知の内容を書き込む
for (int c = 'A'; c <= 'Z'; ++c) fputc(c, fp);
// 先頭に戻って位置10へ移動(Aを0とするとKの位置)
if (fseek(fp, 10L, SEEK_SET) != 0) { perror("fseek"); return 1; }
// 現在位置を確認
long pos = ftell(fp);
if (pos == -1L) { perror("ftell"); return 1; }
printf("現在位置: %ld\n", pos);
// その位置から1文字読む(想定: K)
int ch = fgetc(fp);
if (ch == EOF) { perror("fgetc"); return 1; }
printf("読み取った文字: %c\n", ch);
fclose(fp);
return 0;
}
現在位置: 10
読み取った文字: K
fseekの基本
fseekの引数と書式
fseekはファイルの読み書き位置を移動する関数です。
プロトタイプは次のとおりです。
int fseek(FILE *stream, long offset, int whence);
stream: 対象のFILE*。offset: 起点からのオフセット(バイト数)。負の値も可(起点がSEEK_CURやSEEK_ENDの場合)。whence: 起点を表す定数。
whenceの定数一覧
| 定数 | 起点 | 説明 |
|---|---|---|
SEEK_SET | ファイル先頭 | 先頭からoffsetバイト目へ移動 |
SEEK_CUR | 現在位置 | 現在位置からoffsetだけ前後に移動 |
SEEK_END | ファイル末尾 | 末尾からoffsetだけ前後に移動 |
テキストモードでは改行変換があるため、offsetが実際のバイト位置と一致しないことがあります。
ランダムアクセスはバイナリモード推奨です。
起点
SEEK_SETは絶対位置指定、SEEK_CURとSEEK_ENDは相対位置指定です。
相対指定ではoffsetに負数を使うことで後ろ方向へ移動できます。
移動後の位置が0未満になるような指定は失敗します。
例: 先頭からnバイトへ移動
#include <stdio.h>
int main(void) {
FILE *fp = fopen("seek_from_start.bin", "wb+"); // バイナリ更新モード
if (!fp) { perror("fopen"); return 1; }
// ABC...Zを書き込む
for (int c = 'A'; c <= 'Z'; ++c) fputc(c, fp);
long n = 5; // 先頭から5バイト目(0始まり換算で6文字目)へ
if (fseek(fp, n, SEEK_SET) != 0) { perror("fseek"); fclose(fp); return 1; }
int ch = fgetc(fp); // 期待: 'F'
if (ch == EOF) { perror("fgetc"); fclose(fp); return 1; }
long pos = ftell(fp); // 読み込み後は1つ進む
if (pos == -1L) { perror("ftell"); fclose(fp); return 1; }
printf("先頭から%ldバイトの文字: %c\n", n, ch);
printf("現在位置: %ld\n", pos);
fclose(fp);
return 0;
}
先頭から5バイトの文字: F
現在位置: 6
先頭基準の絶対位置指定はSEEK_SETです。
0を指定すると先頭、1なら2文字目という具合に、0始まりのインデックスと対応します。
例: 現在位置から前後へ移動
#include <stdio.h>
int main(void) {
FILE *fp = fopen("seek_from_cur.bin", "wb+");
if (!fp) { perror("fopen"); return 1; }
// ABC...Z
for (int c = 'A'; c <= 'Z'; ++c) fputc(c, fp);
// 先頭に戻る
if (fseek(fp, 0L, SEEK_SET) != 0) { perror("fseek"); fclose(fp); return 1; }
// 現在位置(0)から+10へ移動 -> 'K'の位置
if (fseek(fp, 10L, SEEK_CUR) != 0) { perror("fseek+10"); fclose(fp); return 1; }
int ch1 = fgetc(fp); // 期待: 'K'
if (ch1 == EOF) { perror("fgetc"); fclose(fp); return 1; }
// 現在位置は11。そこから-3戻る -> 位置8('I')
if (fseek(fp, -3L, SEEK_CUR) != 0) { perror("fseek-3"); fclose(fp); return 1; }
int ch2 = fgetc(fp); // 期待: 'I'
if (ch2 == EOF) { perror("fgetc"); fclose(fp); return 1; }
printf("前方へ10: %c, 後方へ3: %c\n", ch1, ch2);
fclose(fp);
return 0;
}
前方へ10: K, 後方へ3: I
現在位置基準の相対移動はSEEK_CURです。
読み込み後は自動的に位置が進む点に注意してください。
必要に応じてftellで位置を確認すると安全です。
例: 末尾から逆方向へ移動
#include <stdio.h>
int main(void) {
FILE *fp = fopen("seek_from_end.bin", "wb+");
if (!fp) { perror("fopen"); return 1; }
const char *s = "Hello World!\n"; // 末尾は改行
fputs(s, fp);
// 末尾から-2 -> '!' の位置(末尾は終端を意味するため、-1で改行、-2で '!' )
if (fseek(fp, -2L, SEEK_END) != 0) { perror("fseek"); fclose(fp); return 1; }
int ch = fgetc(fp); // 期待: '!'
if (ch == EOF) { perror("fgetc"); fclose(fp); return 1; }
printf("末尾から2バイト目の文字: %c\n", ch);
fclose(fp);
return 0;
}
末尾から2バイト目の文字: !
末尾基準はSEEK_ENDです。
テキストモードでは改行変換のため、この種の計算は安全ではありません。
必ずバイナリモードで行いましょう。
戻り値とエラー処理
fseekは成功で0、失敗で非0を返しerrnoを設定します。
例えば、シークできないストリームや範囲外の位置を指定した場合に失敗します。
エラー表示にはperrorが便利です。
サンプルコード(エラーパターンの扱い)
#include <stdio.h>
#include <errno.h>
int main(void) {
// 標準入力は多くの環境でシーク不可
if (fseek(stdin, 0L, SEEK_SET) != 0) {
perror("stdinへのfseek失敗");
// errnoがESPIPE(Illegal seek)になる環境が多い
}
return 0;
}
stdinへのfseek失敗: Illegal seek
エラーの詳細判定が必要ならerrnoを調べますが、まずはperrorで人間にわかるメッセージを出すことから始めると良いです。
ftellの基本
現在のファイル位置を取得
ftellは先頭からの現在位置をバイト単位で返します。
読み書きによって位置は自動的に進むため、チェックに活用します。
long pos = ftell(fp);
if (pos == -1L) { perror("ftell"); /* エラー */ }
戻り値の型と失敗時
ftellの戻り値はlongで、失敗時は-1Lです。
非常に大きなファイルではlongに収まらない場合があります。
その場合、環境が対応していればfseekoとftello(off_tを使う)の利用を検討してください。
例: 位置を保存して戻る
#include <stdio.h>
int main(void) {
FILE *fp = fopen("bookmark.bin", "wb+");
if (!fp) { perror("fopen"); return 1; }
// 0〜9を書き込む
for (int c = '0'; c <= '9'; ++c) fputc(c, fp);
// 位置3へ移動して保存
if (fseek(fp, 3L, SEEK_SET) != 0) { perror("fseek"); fclose(fp); return 1; }
long saved = ftell(fp);
if (saved == -1L) { perror("ftell"); fclose(fp); return 1; }
// 別の処理: 位置8へ移動して1文字読む
if (fseek(fp, 8L, SEEK_SET) != 0) { perror("fseek"); fclose(fp); return 1; }
int ch1 = fgetc(fp); // 期待: '8'
if (ch1 == EOF) { perror("fgetc"); fclose(fp); return 1; }
// 保存した位置へ戻る: 位置3
if (fseek(fp, saved, SEEK_SET) != 0) { perror("fseek(return)"); fclose(fp); return 1; }
int ch2 = fgetc(fp); // 期待: '3'
if (ch2 == EOF) { perror("fgetc"); fclose(fp); return 1; }
printf("保存位置に戻る前: %c, 戻った後: %c\n", ch1, ch2);
fclose(fp);
return 0;
}
保存位置に戻る前: 8, 戻った後: 3
一時的に位置を記録し、処理後に確実に元へ戻したいときはftellとfseekの組み合わせが定番です。
例では「しおり」のように戻れる位置を保存しています。
例: ファイルサイズを求める
#include <stdio.h>
int main(void) {
// デモ用に適当なデータを用意
FILE *fp = fopen("size_demo.bin", "wb+");
if (!fp) { perror("fopen"); return 1; }
for (int i = 0; i < 100; ++i) fputc('A' + (i % 26), fp); // 100バイト
// サイズ取得: 末尾へ -> 位置を読む -> 元の位置に戻す
long saved = ftell(fp); // ここでは100を指している可能性が高い
if (saved == -1L) { perror("ftell(saved)"); fclose(fp); return 1; }
if (fseek(fp, 0L, SEEK_END) != 0) { perror("fseek(end)"); fclose(fp); return 1; }
long size = ftell(fp);
if (size == -1L) { perror("ftell(size)"); fclose(fp); return 1; }
// 位置復元
if (fseek(fp, saved, SEEK_SET) != 0) { perror("fseek(restore)"); fclose(fp); return 1; }
printf("ファイルサイズ: %ld bytes\n", size);
fclose(fp);
return 0;
}
ファイルサイズ: 100 bytes
サイズ取得の定番はfseek(fp, 0, SEEK_END)からのftellです。
終わったら元の位置に戻す配慮を忘れないようにしましょう。
実用パターンと注意点
固定長レコードをランダムアクセス
固定長レコードのファイルでは、N番目のレコードの先頭はN * sizeof(Record)バイト目です。
これをSEEK_SETで指定すれば、任意のレコードを直接読み書きできます。
#include <stdio.h>
#include <string.h>
typedef struct {
int id; // 4バイト想定(環境依存)
char name[16]; // 固定長文字列(終端保証のためゼロ埋め推奨)
} Record;
static void write_record(FILE *fp, long index, const Record *rec) {
// index番目の先頭へ
if (fseek(fp, index * (long)sizeof(Record), SEEK_SET) != 0) { perror("fseek(write)"); return; }
fwrite(rec, sizeof(Record), 1, fp); // 簡略化のためエラーチェック省略
}
static int read_record(FILE *fp, long index, Record *rec) {
if (fseek(fp, index * (long)sizeof(Record), SEEK_SET) != 0) { perror("fseek(read)"); return -1; }
size_t n = fread(rec, sizeof(Record), 1, fp);
return (int)n; // 1なら成功
}
int main(void) {
FILE *fp = fopen("records.bin", "wb+"); // バイナリ更新モード
if (!fp) { perror("fopen"); return 1; }
Record a = {1, "Alice"}; // 必要に応じてnameをゼロ埋め
Record b = {2, "Bob"};
Record c = {3, "Carol"};
// 3件書く
write_record(fp, 0, &a);
write_record(fp, 1, &b);
write_record(fp, 2, &c);
// 2番目(index=1)だけ読み出す
Record out = {0};
if (read_record(fp, 1, &out) == 1) {
printf("id=%d, name=%s\n", out.id, out.name);
}
fclose(fp);
return 0;
}
id=2, name=Bob
レコードサイズが一定なら単純な掛け算でオフセットが求まるため、データベース風のアクセスが可能です。
ただしstructのメモリ配置はパディングで環境差が出る可能性があるため、実運用ではフィールド単位で書くか、固定幅整数型とパッキング指定を用いると安全です。
バイナリモードで開くと安全
ランダムアクセスはバイナリモードで開くのが基本です。
Windowsのテキストモードでは\nが\r\nに変換され、fseekやftellで扱うバイト位置と見た目の文字数が一致しません。
"rb"や"r+b"を使い、位置とバイト数を一致させましょう。
シークできないストリームに注意
パイプやソケット、端末などシークできないストリームに対するfseekは失敗します。
例えば標準入力stdinがパイプから供給されている場合などです。
失敗時はperrorでIllegal seekなどのメッセージが表示されます。
これはOSの制約であり、順次処理やバッファに読み込んでからシーク可能な一時ファイルへ書き出すなどの対策が必要です。
範囲外シークは失敗する
先頭より前(負の絶対位置)へのシークは失敗します。
一方、SEEK_SETでファイル末尾よりも大きい位置を指定することは、環境によっては成功し、後で書き込みを行うと「穴あきファイル」が生まれることがあります。
読み取り目的なら、範囲チェックをしてからfseekするのが堅実です。
サンプルコード(範囲外シークの検出)
#include <stdio.h>
int main(void) {
FILE *fp = fopen("small.bin", "wb+");
if (!fp) { perror("fopen"); return 1; }
fputs("abc", fp); // 3バイト
// 先頭より前は失敗
if (fseek(fp, -1L, SEEK_SET) != 0) {
perror("負の絶対位置は不可");
}
fclose(fp);
return 0;
}
負の絶対位置は不可: Invalid argument
エラー表示はperrorが便利
perrorは直前のライブラリエラーを人間が読める形でstderrへ出力します。
初心者のうちは、if (fseek(...) != 0) perror("fseek");のように、perrorで状況を即座に可視化する習慣をつけるとデバッグが大きく捗ります。
サンプルコード(基本形)
#include <stdio.h>
int main(void) {
FILE *fp = fopen("no_such_dir/file.bin", "rb");
if (!fp) {
perror("fopen失敗"); // "fopen失敗: No such file or directory" などが表示される
return 1;
}
fclose(fp);
return 0;
}
まとめ
ファイルの読み書き位置を自在に操る鍵はfseekとftellです。
fseekで位置を移動し、ftellで把握するという基本を押さえれば、先頭からnバイト、現在位置からの前後、末尾からの逆方向など、ニーズに応じたランダムアクセスが可能になります。
実用では、固定長レコードのダイレクトアクセスや、ファイルサイズの計測、末尾チェックなどが代表例です。
最後にもう一度要点をまとめます。
バイナリモードで開くこと、シークできないストリームを避けること、範囲チェックと戻り値の確認を徹底しperrorでエラー内容を把握すること。
この基本を守れば、ランダムアクセスは強力で安全な武器になります。
