ファイル入出力

ファイルは金庫のようなものです——使う前に開け(fopen)、出し入れのルールに従い(読み書きモード)、使い終わったら必ず閉め(fclose)なければ、データが失われる恐れがあります。

ファイルの基本概念

C言語はすべての外部デバイスを「ストリーム」として扱います。ファイルストリームには2種類あります:

オペレーティングシステムはFILE構造体で開いているファイルを管理し、私たちはFILE *ポインタを通じて操作します。

ファイルポインタ

C
FILE *fp;

このポインタは、その後のすべてのファイル操作の「鍵」です。これなしでは何もできません。

ファイルのオープンとクローズ

fopen

C
FILE *fopen(const char *filename, const char *mode);
モード 意味 ファイルが存在しない場合 ファイルが存在する場合
"r" 読み取り専用 エラー 先頭から読み取り
"w" 書き込み専用 作成 内容を切り詰め
"a" 追記 作成 末尾に追記
"r+" 読み書き エラー 先頭から操作
"w+" 読み書き 作成 内容を切り詰め
"a+" 読み取りと追記 作成 末尾に追記
"rb" バイナリ読み取り専用 エラー 先頭から読み取り
"wb" バイナリ書き込み専用 作成 内容を切り詰め
"ab" バイナリ追記 作成 末尾に追記
💡 ヒント: "w"モードは既存のファイルを切り詰めます!初心者が最もよく犯す間違いであり、追記のつもりで"w"を使うと、すべてのデータが失われます。

fclose

C
int fclose(FILE *fp);

ファイルを閉じるとバッファがフラッシュされ、リソースが解放されます。成功すると0、失敗するとEOFを返します。

C
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.txt", "w");
    if (fp == NULL) {
        printf("ファイルを開けませんでした\n");
        return 1;
    }
    fprintf(fp, "Hello, File!\n");
    fclose(fp);
    return 0;
}
⚠️ 注意: すべてのfopenには対応するfcloseが必要です。ファイルのクローズ忘れはメモリリークやデータ損失を引き起こします。

テキストファイルの入出力

文字入出力:fgetc / fputc

C
int fgetc(FILE *fp);
int fputc(int ch, FILE *fp);

fgetcは1文字読み取り、ファイル終端でEOFを返します。fputcは1文字書き込みます。

文字列入出力:fgets / fputs

C
char *fgets(char *str, int n, FILE *fp);
int fputs(const char *str, FILE *fp);

fgetsは最大n-1文字を読み取り、改行またはファイル終端で停止し、自動的に'\0'を付加します。

C
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("poem.txt", "w");
    if (fp == NULL) {
        return 1;
    }
    fputs("夕暮れの河辺\n", fp);
    fputs("河は海へと流れる\n", fp);
    fclose(fp);

    fp = fopen("poem.txt", "r");
    if (fp == NULL) {
        return 1;
    }
    char line[256];
    while (fgets(line, sizeof(line), fp) != NULL) {
        printf("%s", line);
    }
    fclose(fp);
    return 0;
}
TEXT
夕暮れの河辺
河は海へと流れる

バイナリファイルの入出力

fread / fwrite

C
size_t fread(void *ptr, size_t size, size_t count, FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *fp);

パラメータ:ptrはデータバッファ、sizeは各要素のバイトサイズ、countは要素数です。戻り値は実際に読み書きされた要素数です。

C
#include <stdio.h>

typedef struct {
    int id;
    char name[20];
    float score;
} Student;

int main(void) {
    Student stu = {1, "Alice", 92.5f};

    FILE *fp = fopen("student.dat", "wb");
    if (fp == NULL) {
        return 1;
    }
    fwrite(&stu, sizeof(Student), 1, fp);
    fclose(fp);

    Student read_stu;
    fp = fopen("student.dat", "rb");
    if (fp == NULL) {
        return 1;
    }
    fread(&read_stu, sizeof(Student), 1, fp);
    fclose(fp);

    printf("ID: %d, Name: %s, Score: %.1f\n",
           read_stu.id, read_stu.name, read_stu.score);
    return 0;
}
TEXT
ID: 1, Name: Alice, Score: 92.5
💡 ヒント: バイナリモードは構造体データの保存に最適であり、効率的で内容を正確に保持できます。テキストモードはOSによって改行文字の処理が異なる場合がありますが、バイナリモードはすべてそのまま保存します。

フォーマット入出力

fprintf / fscanf

C
int fprintf(FILE *fp, const char *format, ...);
int fscanf(FILE *fp, const char *format, ...);

使い方はprintf/scanfと同じで、ファイルポインタのパラメータが追加されているだけです。

C
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("scores.txt", "w");
    if (fp == NULL) {
        return 1;
    }
    fprintf(fp, "%s %d %.1f\n", "Bob", 2, 88.0);
    fprintf(fp, "%s %d %.1f\n", "Carol", 3, 95.5);
    fclose(fp);

    fp = fopen("scores.txt", "r");
    if (fp == NULL) {
        return 1;
    }
    char name[20];
    int id;
    float score;
    while (fscanf(fp, "%s %d %f", name, &id, &score) == 3) {
        printf("Name: %s, ID: %d, Score: %.1f\n", name, id, score);
    }
    fclose(fp);
    return 0;
}
TEXT
Name: Bob, ID: 2, Score: 88.0
Name: Carol, ID: 3, Score: 95.5

ファイル位置ポインタ

開いているファイルにはすべて「位置ポインタ」があり、現在の読み書き位置を示しています。読書のしおりのようなもので、どこまで読んだかをしおりが示しています。

fseek

C
int fseek(FILE *fp, long offset, int origin);
origin 意味
SEEK_SET ファイルの先頭
SEEK_CUR 現在の位置
SEEK_END ファイルの末尾

ftell / rewind

C
long ftell(FILE *fp);
void rewind(FILE *fp);

ftellは現在の位置(ファイル先頭からのバイトオフセット)を返します。rewindfseek(fp, 0, SEEK_SET)と等価です。

fseekftellを使ってファイルサイズを計算します:

C
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.txt", "rb");
    if (fp == NULL) {
        printf("ファイルを開けませんでした\n");
        return 1;
    }
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    rewind(fp);
    printf("ファイルサイズ: %ld バイト\n", size);
    fclose(fp);
    return 0;
}
▶ 試してみよう

バイナリファイル内の特定レコードへのランダムアクセス:

C
#include <stdio.h>

typedef struct {
    int id;
    char name[20];
    float score;
} Student;

int main(void) {
    Student students[3] = {
        {1, "Alice", 92.5f},
        {2, "Bob", 88.0f},
        {3, "Carol", 95.5f}
    };

    FILE *fp = fopen("students.dat", "wb");
    if (fp == NULL) {
        return 1;
    }
    fwrite(students, sizeof(Student), 3, fp);
    fclose(fp);

    fp = fopen("students.dat", "rb");
    if (fp == NULL) {
        return 1;
    }
    int target = 2;
    fseek(fp, (target - 1) * sizeof(Student), SEEK_SET);
    Student s;
    fread(&s, sizeof(Student), 1, fp);
    fclose(fp);

    printf("Record %d: ID=%d, Name=%s, Score=%.1f\n",
           target, s.id, s.name, s.score);
    return 0;
}
▶ 試してみよう
TEXT
Record 2: ID=2, Name=Bob, Score=88.0
💡 ヒント: これがデータベースにおける「ランダムアクセス」の原理です!オフセットを計算することで、先頭から走査せずに特定の位置へ直接ジャンプできます。

その他のファイル関数

feof

C
int feof(FILE *fp);

ファイル終端に達したかどうかを確認します。EOFで非ゼロを返します。注意:feofは読み取り操作が失敗した後でのみ真になり、事前チェックとしては使えません。

fflush

C
int fflush(FILE *fp);

バッファを強制的にフラッシュし、バッファリングされたデータをファイルに書き込みます。入力ストリームに対してfflushを呼び出すと未定義動作になります。

remove / rename

C
int remove(const char *filename);
int rename(const char *old, const char *new);

ファイルの削除と名前変更です。成功すると0を返します。

ファイル操作の基本パターン

ほとんどのファイル操作は次のパターンに従います:

  1. fopenでファイルを開き、戻り値を確認する
  2. ループでデータを読み書きする
  3. fcloseでファイルを閉じる
C
#include <stdio.h>

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

    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }

    fclose(fp);
    return 0;
}
⚠️ 注意: fopenの戻り値確認は絶対的なルールです!ファイルが存在しない、権限がない、ディスクがいっぱい等の可能性があり——確認せずにヌルポインタを使うと即座にクラッシュします。

❓ よくある質問

Q fopenがNULLを返すのに、ファイルは確かに存在します。何が問題でしょうか?
A おそらくパスの問題です。相対パスはプログラムの作業ディレクトリに基づき、ソースファイルのディレクトリではありません。IDEで実行する場合、作業ディレクトリが期待と異なる場合があります。
Q テキストモードとバイナリモードの違いは何ですか?
A Windowsでは、テキストモードは読み書き時に\n\r\nの変換を行いますが、バイナリモードは変換を行いません。Linuxでは両者に違いはありません。
Q fwriteで書き出したファイルは別のコンパイラと互換性がありますか?
A 必ずしもそうとは限りません。メモリアライメント、バイトオーダー(エンディアン)、基本型のサイズが異なる場合があります。クロスプラットフォームのコードでは手動シリアライズが必要です。
Q なぜfeofをループ条件に使ってはいけないのですか?
A feofは読み取り操作がファイル終端を越えた後でのみ真になります。ループ条件として使うと、最後の反復で不正なデータを処理してしまいます。fread/fgetsの戻り値を使ってください。

📖 まとめ

📝 練習問題

  1. テキストファイルの行数、単語数、文字数を数えるプログラムを作成してください
  2. バイナリモードでソースファイルを読み取り、宛先ファイルに書き出す簡単なファイルコピープログラムを作成してください
  3. 構造体配列をバイナリファイルに保存し、再度読み出して表示するプログラムを作成してください
100%