閉じる

【C言語】rewindでファイル位置を先頭に戻す正しい書き方と注意点

C言語でファイルを扱うとき、途中まで読み進めた位置を先頭に戻したくなる場面は少なくありません。

その代表的な手段がrewind関数です。

本記事では、rewindの基本から、ftell・fseekとの違い、テキストファイルとバイナリファイルの注意点、さらにマルチスレッド環境での危険性まで、実務で迷わないための知識を丁寧に解説します。

rewindとは

rewind関数の役割と特徴

C言語のrewind関数は、ストリーム(ファイル)の読み書き位置を先頭に戻す標準ライブラリ関数です。

さらに、位置を戻すだけでなく、EOFフラグとエラーフラグをクリアするという重要な副作用も持っています。

主な特徴を文章で整理すると、次のようになります。

1つ目は、引数にとるFILE *(ファイルポインタ)が指すストリームのファイル位置インジケータを先頭に移動することです。

これにより、次の読み込みや書き込みはファイルの先頭から行われます。

2つ目は、エラーの戻り値を返さないことです。

rewindの返り値はvoidであり、成功・失敗を直接判定できません。

この点がfseekとの大きな違いです。

3つ目として、EOFフラグとエラーフラグを同時にクリアする副作用があります。

これは、後でferrorfeofによって状態を確認しているコードと組み合わせる際に、挙動を理解しておくべき重要ポイントです。

標準的な宣言は次のようになっています。

C言語
#include <stdio.h>

void rewind(FILE *stream);

ftellやfseekとの違い

rewindとよく比較される関数にfseekftellがあります。

これらはすべてファイル位置インジケータに関わる関数ですが、役割は少しずつ違います。

まず、ftell現在のファイル位置を取得する関数です。

戻り値としてlong型の位置(オフセット)が返ってきます。

一方、rewind現在位置を取得する機能はなく、単に先頭に戻すだけです。

次に、fseek任意の位置へファイル位置インジケータを移動する関数であり、先頭だけでなく、中間や末尾付近にも移動できます。

rewindは「先頭に固定」の簡易版、fseekは「柔軟に位置指定できる」上位版と見ることもできますが、EOF・エラー状態の扱いが異なります。

rewindとfseekの挙動の違いは、次の表のように整理できます。

関数名役割移動可能な位置戻り値EOF/エラー状態への影響
rewind先頭に戻す先頭のみなし(void)EOFとエラーをクリア
fseek任意位置へ移動先頭/中間/末尾近辺成功/失敗(int)原則クリアしない(実装依存も絡む)
ftell現在位置を取得位置を取得のみ位置(long)状態は変えない

「位置を動かしたいだけか」、「状態フラグもリセットしたいのか」という観点で、rewindとfseekを使い分けることが重要です。

テキストファイルとバイナリファイルでの扱い

C言語では、ファイルを"r""w"などのテキストモード、または"rb""wb"などのバイナリモードで開くことができます。

rewindはどちらのモードでも利用できますが、特にテキストモードでは改行コードの変換が絡むため、ftell/fseekとの組み合わせが微妙に難しくなります。

テキストモードでは、OSによって改行がLFだったりCRLFだったりします。

ライブラリが自動的に変換を行うため、ftellで取得した位置と実際のバイトオフセットの対応が実装依存になる場合があります。

しかし、rewindは常に論理的な先頭位置(読み書きが始まる最初の位置)に戻るだけなので、テキスト・バイナリの違いによる影響は比較的小さいといえます。

ただし、後述する「テキストモードと改行コード変換による注意点」で触れるように、大量のランダムアクセスを行う場合や、オフセット値をファイル間で共有する場合は慎重さが求められます。

rewindの正しい使い方

rewindの基本的な書き方と引数FILEポインタ

rewindの基本的な使い方は非常にシンプルで、引数として有効なFILE *を1つ渡すだけです。

典型的なコードの流れは次のようになります。

C言語
#include <stdio.h>

int main(void) {
    // ファイルを読み込み専用で開く
    FILE *fp = fopen("sample.txt", "r");
    if (fp == NULL) {
        perror("fopen error");
        return 1;
    }

    // ここで何らかの読み込み処理を行う
    // ...

    // ファイル位置を先頭に戻す
    rewind(fp);

    // 再度、先頭から読み込み処理を行う
    // ...

    // ファイルを閉じる
    fclose(fp);

    return 0;
}

rewindのシグネチャがvoid rewind(FILE *stream);であることからも分かるように、戻り値によるエラー判定はできません

ファイルがすでに閉じられているFILE *NULLを渡してしまうと未定義動作になりますので、呼び出し前の管理が特に重要です。

読み込み専用ファイルでrewindを使う例

ここでは、テキストファイルを読み込み専用"r"で開き、ファイル全体を2回読む簡単なサンプルを示します。

1回目に内容を表示し、rewindで先頭に戻したあと、2回目も同じように表示します。

C言語
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.txt", "r");  // 読み込み専用で開く
    if (fp == NULL) {
        perror("fopen error");
        return 1;
    }

    int ch;

    // 1回目の読み込み
    printf("=== 1st read ===\n");
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }

    // ここでEOFに達しているので、rewindで先頭へ戻す
    rewind(fp);

    printf("\n=== 2nd read ===\n");
    // 2回目の読み込み
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }

    fclose(fp);
    return 0;
}

実行例として、data.txtに次の内容が入っていたとします。

Hello
World

上記プログラムの出力は次のようになります。

実行結果
=== 1st read ===
Hello
World

=== 2nd read ===
Hello
World

この例では、1回目の読み込みループ終了時点でEOFに達しており、ストリームにはEOFフラグが立っています。

rewindを呼ぶことで位置が先頭に戻ると同時にEOFフラグもクリアされるため、2回目も問題なく読み込みが行えることが分かります。

読み書き両用ファイルでrewindを使う例

読み書き両用モード"r+""w+"などで開いたファイルに対しても、rewindは利用できます。

ただし、読みと書きが混在する場合、バッファリングや未フラッシュのデータに気を付ける必要があります。

この点は「バッファリングと未書き込みデータへの影響」で詳しく扱います。

ここではまず、シンプルな読み書き両用の例を示します。

C言語
#include <stdio.h>

int main(void) {
    // 読み書き両用で開く(既存ファイルがある前提)
    FILE *fp = fopen("rw_sample.txt", "r+");
    if (fp == NULL) {
        perror("fopen error");
        return 1;
    }

    // 先頭から一行読み込んで表示する
    char buf[256];
    if (fgets(buf, sizeof(buf), fp) != NULL) {
        printf("1行目: %s", buf);
    }

    // ファイルの末尾に文字列を追記してみる
    // ただし "r+" なので、fseekで末尾に移動してから書き込む
    if (fseek(fp, 0, SEEK_END) != 0) {
        perror("fseek error");
        fclose(fp);
        return 1;
    }
    fputs("\nADD LINE", fp);  // 末尾に新しい行を追加

    // 再び先頭に戻して、全体を読み直す
    rewind(fp);

    printf("\n=== 全体を読み直し ===\n");
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        fputs(buf, stdout);
    }

    fclose(fp);
    return 0;
}

このプログラムでは、先頭から1行読み込み、その後fseekで末尾に移動して追記し、最後にrewindで先頭に戻して全体を読み直します。

ここで重要なのは、書き込み後に必ず明示的に位置を戻してから読み直すことです。

rewindはその役割を簡潔に果たしてくれます。

エラー確認にclearerrとferrorを併用する方法

rewindはEOFフラグとエラーフラグを自動的にクリアします。

その一方で、fseekはフラグに直接影響しないことが多く、エラー状態が残ったままになる場合があります。

そのため、エラー確認やリトライ処理を行いたい場合には、clearerrferrorと組み合わせると挙動を理解しやすくなります。

次のサンプルでは、fgetcでEOFに達したあと、rewindやclearerrを使って再読み込みする挙動を確認します。

C言語
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) {
        perror("fopen error");
        return 1;
    }

    int ch;

    // ファイルを最後まで読み切る
    while ((ch = fgetc(fp)) != EOF) {
        // 何もしない
    }

    // ここでEOFに達しているので、EOFフラグが立っている
    if (feof(fp)) {
        printf("EOFフラグが立っています\n");
    }
    if (ferror(fp)) {
        printf("エラーフラグが立っています\n");
    }

    // rewindで先頭に戻す(EOF・エラーフラグもクリアされる)
    rewind(fp);

    if (!feof(fp) && !ferror(fp)) {
        printf("rewind後はEOF・エラーともにクリアされています\n");
    }

    // 再度、最後まで読み切る
    while ((ch = fgetc(fp)) != EOF) {
        // 何もしない
    }

    // 今度はclearerrでフラグだけクリアしてみる
    clearerr(fp);

    if (!feof(fp) && !ferror(fp)) {
        printf("clearerr後もEOF・エラーともにクリアされています\n");
    }

    fclose(fp);
    return 0;
}

このサンプルでは、rewindとclearerrの両方がEOF・エラーフラグをクリアすることが確認できます。

ただし、rewindは位置も先頭に戻すのに対し、clearerr位置を一切動かさない点が決定的な違いです。

位置も含めてリセットしたい場合はrewind、位置はそのままで状態だけリセットしたい場合はclearerrと覚えておくと整理しやすくなります。

rewind使用時の注意点と落とし穴

EOFフラグとエラーフラグをクリアする挙動

先ほども触れましたが、rewindはEOFフラグとエラーフラグをクリアするという副作用を持ちます。

この挙動は便利な反面、「いつフラグが消されたのか」を意識していないとデバッグを難しくする要因にもなります。

例えば、ある処理でfreadがエラーになりferror(fp)が真になったとします。

その後に、位置を先頭に戻したいという理由だけでrewindを呼ぶと、エラーフラグも同時にクリアされてしまいます

結果として、後続の診断コードが「エラーが起きていない」と誤解する可能性があります。

このような場面では、エラー情報をログに残す、あるいはerrnoや独自フラグに退避してからrewindを呼ぶなど、フラグ消失を前提とした設計を行うことが重要です。

バッファリングと未書き込みデータへの影響

Cの標準入出力は、ほとんどの場合バッファリングされています。

特にstdoutやファイルストリームでは、fwritefprintfで書いたデータが即座にディスクへ書き込まれず、内部バッファに滞留していることがあります。

この状態でrewindを呼ぶとどうなるかは、実装やモードによって異なりますが、一般的な注意点として「読み書きを切り替える前にはfflushかfseekが必要」というルールを理解しておく必要があります。

標準では、r+w+などの更新モードで、読み操作から書き操作、または書き操作から読み操作へ切り替える前にfflushfseekrewindなどの位置操作を行わなければ未定義動作になると規定されています。

rewindも位置操作の一種としてカウントされますが、未フラッシュのデータを安全に扱う意味では、書き込み後にfflushを明示的に呼ぶことが推奨されます。

例として、書き込み後にrewindしてから読み込むケースを考えます。

C言語
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("buf_test.txt", "w+");  // 読み書き両用で開く
    if (fp == NULL) {
        perror("fopen error");
        return 1;
    }

    // ファイルにデータを書き込む
    fputs("ABC\n", fp);

    // 書き込み後、読み込みに切り替える前にfflushを呼ぶのが安全
    fflush(fp);

    // 先頭に戻してから読み込む
    rewind(fp);

    char buf[16];
    if (fgets(buf, sizeof(buf), fp) != NULL) {
        printf("読み込んだ文字列: %s", buf);
    }

    fclose(fp);
    return 0;
}

ここではfflush(fp);を挟んだ上でrewindを呼んでいます。

「fflush → rewind → 読み込み」という順番を守ることで、未書き込みデータの取りこぼしを防げます。

追記モード(aモード)ファイルでrewindが効かない理由

追記モード"a""a+"で開いたファイルに対してrewindを使うと、「位置が先頭に戻ったはずなのに、なぜか追記されてしまう」という混乱がよく起こります。

標準の規定では、追記モードではすべての書き込み操作がファイル末尾に対して行われるとされています。

つまり、rewindやfseekで論理上の位置を先頭に移動しても、書き込み時には必ず末尾に強制的に移動されるということです。

この挙動を確認する簡単な例を示します。

C言語
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("append_test.txt", "a+");  // 追記モード(読み書き両用)
    if (fp == NULL) {
        perror("fopen error");
        return 1;
    }

    // 先頭に戻してから内容を読み出す
    rewind(fp);

    printf("=== 現在の内容 ===\n");
    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }

    // 再び先頭に戻してから書き込んでみる
    rewind(fp);
    fputs("\nLINE FROM REWIND", fp);

    fclose(fp);
    return 0;
}

このコードでは、2回目のrewind(fp);で先頭に戻したあとにfputsを呼んでいますが、実際には常にファイル末尾に追記されます。

rewindで影響するのは主に読み込み位置であり、追記モードの書き込み位置には影響しない点が最大の注意点です。

「追記モードで既存部分を上書きしたい」という要件がある場合、そもそも"a"モードではなく"r+""w+"を検討すべきです。

テキストモードと改行コード変換による注意点

テキストモードでファイルを開くと、特にWindows環境では改行コードの自動変換が行われます。

プログラム側では'\n'という1文字を書いたつもりでも、実際のファイルにはCRLFの2バイトが書き込まれます。

このため、ftellが返す位置はライブラリ内部の論理的な位置であり、必ずしも物理的なバイトオフセットと一致しません。

rewind自体は常に「論理的な先頭」に戻るだけなので、テキストモードでも比較的安全に使えます。

しかし、fseekと組み合わせて「ftellで位置を保存しておき、後でその値に戻る」といった使い方をするときは、テキストモードでは移植性や挙動が実装依存となる可能性があります。

大まかな指針としては、次のように考えるとよいです。

  • テキストモードでは、ファイル全体を順に読む処理(1行ずつ読むなど)にとどめ、ランダムアクセスを多用しない。
  • ランダムアクセスやバイナリ構造体の読み書きが必要な場合はバイナリモード(例: "rb", "wb")で開く。
  • rewindで先頭に戻す程度であれば、テキストモードでもほぼ問題なく利用できる

マルチスレッド環境でのFILE共有とrewindの危険性

マルチスレッド環境では、1つのFILEポインタを複数スレッドで共有するケースがしばしば見られます。

しかし、rewindのようなファイル位置を変更する操作を複数スレッドから同時に行うと、予測不能な挙動を招く原因となります。

例えば、スレッドAがファイルを順に読み進めている途中で、スレッドBが同じFILE *に対してrewindを呼ぶと、スレッドAの読み位置も先頭に飛んでしまうことになります。

その結果、同じデータを何度も読んでしまったり、一部がスキップされたりと、データ処理の整合性が崩れます。

対策としては、次のような設計が考えられます。

1つ目は、スレッドごとに別々にファイルを開く方法です。

この場合、各スレッドはそれぞれ独立したFILE *を扱うため、rewindなどの位置操作が互いに干渉しません。

2つ目は、やむを得ずFILE *を共有する場合に、ミューテックスなどでアクセスを逐次的に保護する方法です。

この場合も、特にrewindやfseekなどの位置操作を行う範囲はスレッド間で<strong>排他的に</strong>扱う必要があります。

3つ目として、そもそもFILEを直接共有せず、スレッドセーフなI/O抽象化レイヤを自作するという設計もあります。

例えば、読込専用キューにファイルの一部を読み込んだチャンクを渡し、各スレッドはキューからデータを消費するだけにする方法などが考えられます。

マルチスレッドでrewindを利用する際は、「スレッドから見たファイル位置の一貫性」に特に注意してください。

rewindとfseekの使い分け

ファイル先頭に戻すだけならrewindを使うべきケース

rewindとfseekのどちらを使うべきか迷ったとき、「先頭に戻すだけならrewindでよいか」という観点は有用です。

例えば、次のようなパターンではrewindが自然です。

  • ファイルを最後までスキャンして統計情報をとり、その後もう一度最初から読み込む。
  • 1回目の読みで形式チェックを行い、2回目に本処理を行う。
  • EOFまで読み切ったあと、再度同じ内容を別のデータ構造に読み込みたい。

このようなケースでは、fseek(fp, 0L, SEEK_SET)と書く代わりにrewind(fp);と書いた方が、「先頭に戻す」という意図がコードから一目でわかるため、可読性にも優れています。

また、同時にEOF・エラーフラグもクリアされるため、「2回目の読み込み時にEOFフラグが残っていてループに入れない」といったトラブルも防げます。

読み込み位置を柔軟に制御したい場合のfseek

一方で、ファイル内の任意の位置にジャンプしたい場合は、rewindではなくfseekが必須です。

例えば、固定長レコードを持つバイナリファイルからn番目のレコードを読む場合、fseekを使ってオフセットを計算し、その位置にシークする必要があります。

C言語
#include <stdio.h>

typedef struct {
    int id;
    char name[32];
} Record;

int main(void) {
    FILE *fp = fopen("record.bin", "rb");
    if (fp == NULL) {
        perror("fopen error");
        return 1;
    }

    long index = 10;  // 11番目のレコードを読みたい(0始まりとする)
    long offset = index * (long)sizeof(Record);

    // 指定したレコード位置に移動する
    if (fseek(fp, offset, SEEK_SET) != 0) {
        perror("fseek error");
        fclose(fp);
        return 1;
    }

    Record rec;
    if (fread(&rec, sizeof(Record), 1, fp) == 1) {
        printf("id=%d, name=%s\n", rec.id, rec.name);
    } else {
        printf("レコードの読み込みに失敗しました\n");
    }

    fclose(fp);
    return 0;
}

このようなランダムアクセスでは、相対位置指定(SEEK_SET, SEEK_CUR, SEEK_END)を駆使する必要があり、rewindでは代替できません。

rewindは「常に先頭に戻す」という単純な操作しか持たないため、柔軟な位置制御には向きません。

大規模ファイル処理でのパフォーマンスと設計指針

大規模なファイル(数GB〜数十GB)を扱う場合、ファイルを何度も先頭から読み直す設計はパフォーマンスのボトルネックになりやすくなります。

rewind自体は単に位置情報をリセットするだけなので軽い処理ですが、「先頭から再度読み直す」というI/Oコストは非常に重くなります。

そのため、設計段階で次のような指針を意識することが重要です。

1つ目は、可能な限り1パスで処理を完結させることです。

例えば、1回目の読み込みで統計情報を集計し、2回目に実行処理を行う設計になっている場合、1回目の段階で情報を保存しておき、2回目のパスを不要にするといった工夫ができないかを検討します。

2つ目は、どうしても2パス以上が必要な場合、メモリキャッシュやインデックスファイルを併用することです。

一度読んだメタデータだけを別ファイルやメモリに保持し、2回目以降はそれを利用することで、ファイル全体の再読み込み回数を減らせます。

3つ目として、シーク回数やシークパターンを減らすことも重要です。

特にHDD環境では、ランダムシークが遅い原因となります。

可能ならアクセスを順次的(シーケンシャル)に保つように設計し、どうしてもランダムアクセスが必要な部分にだけfseekを使い、先頭に戻る必要がある場合にはrewindを用いる、というメリハリが大切です。

まとめると、rewindは設計上「やむを得ず複数パスを回す」場面での補助として考え、基本方針としては「できるだけ1パスで処理を終わらせる」ことを目標にするのが、大規模ファイル処理のパフォーマンス最適化につながります。

まとめ

rewindは「ファイル位置を先頭に戻し、EOF・エラーフラグをクリアする」という、シンプルながら強力な機能を持つ関数です。

単に先頭へ戻すだけならfseekよりも意図が明確で、読み直し処理などに向いています。

一方で、追記モードでは書き込み位置に影響しないこと、バッファリングやマルチスレッド環境での共有など、いくつかの落とし穴も存在します。

ftell・fseekとの役割の違い、テキスト/バイナリモードにおける注意点を理解したうえで、「先頭に戻すだけの場面ではrewind」「柔軟な位置制御が必要な場面ではfseek」と使い分けることで、安全かつ読みやすいファイルI/Oコードを書けるようになります。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!