C言語のきほん|バイナリファイルの実践問題

バイナリファイルを使いこなせると、C言語の保存処理はぐっと実践的になる。

ここまで学んできたファイル入出力では、テキストファイルを使って文字や文字列を保存する方法、そしてバイナリファイルを使って構造体や数値データをそのまま保存する方法を見てきました。
その基本をしっかり押さえたあとに大切になるのが、実際に手を動かして問題を解くことです。

バイナリファイルの学習では、ただ fwrite と fread の書き方を覚えるだけでは少し足りません。
実際には、

  • 複数の数値をまとめて保存する
  • 構造体の配列を読み込んで条件で選別する
  • 追記モードでレコードを増やす
  • 1件ずつ fread しながら表示する
  • バイナリデータを16進数で確認する

といった流れの中で理解が深まっていきます。
今回の元文書でも、乱数配列の保存、四角形データの抽出、Person 構造体の追記と表示、さらにファイルダンプ、確認問題まで、段階的に学べる構成になっていました。

特にバイナリファイルは、人が直接読みやすい形ではないぶん、プログラムで正しく保存して、正しく読み戻せるかがとても大切です。
そのため実践問題では、ファイルのオープンモード、構造体サイズ、返却値の確認、1件ずつ読む処理、条件に合うデータだけを書き出す処理など、基礎の知識を組み合わせて考えることになります。元文書でも、生成した整数をそのまま保存する問題、構造体を条件で抽出する問題、毎回オープン・クローズしながら追記と表示を繰り返す問題が示されており、学習の中心が「使い分けと実装」に置かれていることがわかります。

ここでは、そうした流れを踏まえながら、バイナリファイルの実践問題をどう考えるかをやさしく整理し、そのうえで元の問題と似た新しい実践問題と解答例、さらにチャレンジ問題に近い追加問題、最後に確認問題までまとめて作っていきます。内容は、ユーザーが提示した文書の構成と意図を土台にして再構成しています。

バイナリファイルの実践問題で大切な視点

バイナリファイルの問題では、ただデータを保存するだけでなく、どの単位で保存するかを意識することが重要です。

たとえば、次のような観点があります。

観点考えること
保存する単位int 配列なのか、構造体1件なのか、構造体配列全体なのか
オープンモードwb で上書きするのか、ab で追記するのか、rb で読むのか
読み込み方法一括で読むのか、1件ずつ fread するのか
判定処理条件に合うものだけ抽出するのか
確認方法printf で表示するのか、ダンプ表示するのか

元文書でも、乱数を20個保存してまとめて読み込む問題、四角形構造体を条件抽出する問題、Person 型を追記して1件ずつ表示する問題、ファイルダンプの問題が並んでいて、保存単位や読み込み単位が少しずつ違っています。そこがとても学習的です。

まず押さえたい基本パターン

バイナリファイルの問題には、いくつかよく出る基本パターンがあります。

配列をまとめて保存してまとめて読む

これは乱数配列の保存問題のタイプです。
同じ型のデータが連続しているので、配列全体をそのまま fwrite し、同じサイズの配列に fread で戻します。元文書の pr17_2_1.c はまさにこの考え方でした。

構造体を1件ずつ読んで条件で抽出する

これは四角形データから台形だけを抽出する問題のタイプです。
fread で1件ずつ読み込み、条件判定を行い、合格したものだけ fwrite で出力します。元文書でも「台形かどうかを判定する関数を作るといいね」と補足されていました。

構造体を追記してあとで一覧表示する

これは Person 型の問題のタイプです。
1件ずつ入力し、ab モードで追記し、表示時は rb モードで1件ずつ fread しながら出力します。さらに毎回ファイルをオープン・クローズすることも条件として示されていました。

1バイトずつ読んで中身を観察する

これはファイルダンプの問題のタイプです。
バイナリファイルそのものの内容を理解するために、1バイトずつ読みながら 16進数と印字可能文字を表示します。元文書では isprint を使って ASCII 文字表示も加えるよう求められていました。

この図では、バイナリファイルの実践問題でよく出る4つの方向をまとめて表しています。

  • 配列をまとめて保存する
  • 構造体を1件ずつ読む
  • 条件に合うものだけ別ファイルへ書き出す
  • 最後にダンプ表示で中身を確認する

こうして見ると、バイナリファイルの問題は別々のテーマに見えても、実は fread と fwrite を中心にした応用でつながっていることがわかります。

実践問題

0以上499以下のランダムな整数値を15個生成し、それらをバイナリファイル lucky_numbers.dat に書き込みます。その後、ファイルからデータを読み込み、すべての数値を画面に表示し、さらに偶数の個数も表示するプログラムを作成してください。

実行結果例(環境により異なります)

ファイルから読み込んだデータ
42 317 88 154 9 260 401 72 110 95 36 284 17 198 63
偶数の個数:9

解答例

ファイル名:17_7_1.c

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

int main(void)
{
    int numbers[15];
    int loaded[15];
    int even_count = 0;

    srand((unsigned int)time(NULL));

    for (int i = 0; i < 15; i++) {
        numbers[i] = rand() % 500;
    }

    FILE *fout = fopen("lucky_numbers.dat", "wb");
    if (fout == NULL) {
        printf("書き込み用ファイルを開けませんでした。\n");
        return 1;
    }

    if (fwrite(numbers, sizeof(numbers), 1, fout) != 1) {
        printf("データの書き込みに失敗しました。\n");
        fclose(fout);
        return 1;
    }

    fclose(fout);

    FILE *fin = fopen("lucky_numbers.dat", "rb");
    if (fin == NULL) {
        printf("読み込み用ファイルを開けませんでした。\n");
        return 1;
    }

    if (fread(loaded, sizeof(loaded), 1, fin) != 1) {
        printf("データの読み込みに失敗しました。\n");
        fclose(fin);
        return 1;
    }

    fclose(fin);

    printf("ファイルから読み込んだデータ\n");
    for (int i = 0; i < 15; i++) {
        printf("%d ", loaded[i]);
        if (loaded[i] % 2 == 0) {
            even_count++;
        }
    }
    printf("\n偶数の個数:%d\n", even_count);

    return 0;
}

解説

この問題のポイントは、int 配列全体をまとめて書いて、同じ大きさの配列へまとめて戻すことです。
そのため、

fwrite(numbers, sizeof(numbers), 1, fout)

fread(loaded, sizeof(loaded), 1, fin)

の形がとても自然です。

実践問題

バイナリファイル rectangle.dat には、長方形の情報が Rectangle 構造体の形式で格納されています。このファイルを読み込み、面積が100以上のデータのみを large_rectangle.dat に書き出すプログラムを作成してください。なお、面積は width × height で求めるものとします。

typedef struct {
    int x;
    int y;
    int width;
    int height;
} Rectangle;

実行結果例

面積が100以上の長方形
0: x=0, y=0, width=10, height=15
1: x=5, y=8, width=20, height=6
2件をファイルに保存しました。

解答例

ファイル名:17_7_2.c

#include <stdio.h>

typedef struct {
    int x;
    int y;
    int width;
    int height;
} Rectangle;

int is_large_rectangle(Rectangle r)
{
    return r.width * r.height >= 100;
}

int main(void)
{
    FILE *fin = fopen("rectangle.dat", "rb");
    if (fin == NULL) {
        printf("読み込み用ファイルを開けませんでした。\n");
        return 1;
    }

    FILE *fout = fopen("large_rectangle.dat", "wb");
    if (fout == NULL) {
        printf("書き込み用ファイルを開けませんでした。\n");
        fclose(fin);
        return 1;
    }

    Rectangle rect;
    int count = 0;

    printf("面積が100以上の長方形\n");

    while (fread(&rect, sizeof(Rectangle), 1, fin) == 1) {
        if (is_large_rectangle(rect)) {
            printf("%d: x=%d, y=%d, width=%d, height=%d\n",
                   count, rect.x, rect.y, rect.width, rect.height);

            fwrite(&rect, sizeof(Rectangle), 1, fout);
            count++;
        }
    }

    fclose(fin);
    fclose(fout);

    printf("%d件をファイルに保存しました。\n", count);

    return 0;
}

解説

この問題では、ファイル全体を一気に読むのではなく、Rectangle を1件ずつ fread しているのがポイントです。

while (fread(&rect, sizeof(Rectangle), 1, fin) == 1)

という形にすると、ファイル終端まで自然に繰り返せます。
また、元の台形問題と同じように、判定処理を関数に切り出しているので、main 関数の見通しがよくなります。元文書でも、台形かどうかを判定する関数を作るように勧められていました。

実践問題

次の Book 型構造体を使用し、バイナリファイル book.dat にデータを保存・表示するプログラムを作成してください。

typedef struct {
    int id;         // 管理番号
    char title[30]; // 書名
    int price;      // 価格
} Book;

以下のモード番号を選択し、9が入力されるまで繰り返し実行します。なお、ファイルは毎回オープン・クローズしてください。

  • 1:Book データをキーボードから入力し、バイナリファイル book.dat に追記する
  • 2:バイナリファイル book.dat を読み込み、すべてのデータを表示する
  • 9:プログラムを終了する

モード2のときは、1個ずつ fread で構造体を読み込みながら表示してください。

解答例

ファイル名:17_7_3.c

#include <stdio.h>

typedef struct {
    int id;
    char title[30];
    int price;
} Book;

int main(void)
{
    int mode;

    do {
        printf("モードを選択してください\n");
        printf("1:データの追記\n");
        printf("2:ファイル内容の表示\n");
        printf("9:終了\n");
        printf("選択 > ");
        scanf("%d", &mode);

        if (mode == 1) {
            Book book;

            printf("管理番号を入力してください > ");
            scanf("%d", &book.id);

            printf("書名を入力してください > ");
            scanf("%29s", book.title);

            printf("価格を入力してください > ");
            scanf("%d", &book.price);

            FILE *fp = fopen("book.dat", "ab");
            if (fp == NULL) {
                printf("書き込み用ファイルを開けませんでした。\n");
                continue;
            }

            fwrite(&book, sizeof(Book), 1, fp);
            fclose(fp);
        }
        else if (mode == 2) {
            FILE *fp = fopen("book.dat", "rb");
            if (fp == NULL) {
                printf("読み込み用ファイルを開けませんでした。\n");
                continue;
            }

            Book book;
            printf("[book.datの内容]\n");

            while (fread(&book, sizeof(Book), 1, fp) == 1) {
                printf("管理番号:%d,書名:%s,価格:%d円\n",
                       book.id, book.title, book.price);
            }

            fclose(fp);
        }
        else if (mode == 9) {
            printf("終了します。\n");
        }
        else {
            printf("正しい番号を選んでください。\n");
        }

        printf("\n");

    } while (mode != 9);

    return 0;
}

解説

この問題で大事なのは、追記時と表示時でモードが違うことです。

モード番号ファイルモード理由
1ab既存データの末尾に追加したいから
2rb保存済みの全データを読みたいから

実践問題

コマンドラインから指定されたファイル名のファイルを開き、その内容を16進数で表示する簡易ダンププログラムを作成してください。表示は1行に8バイトずつとし、各行の最後に印字可能な文字だけを ASCII 文字で表示してください。印字できない文字は . で表示します。

実行結果例

00000000: 41 42 43 44 31 32 33 34 |ABCD1234|
00000008: 00 0A 7F 61 62 63 20 5A |...abc Z|

解答例

ファイル名:17_7_4.c

#include <stdio.h>
#include <ctype.h>

int main(int argc, char *argv[])
{
    if (argc != 2) {
        printf("使い方: pr17_2_d ファイル名\n");
        return 1;
    }

    FILE *fp = fopen(argv[1], "rb");
    if (fp == NULL) {
        printf("ファイルを開けませんでした。\n");
        return 1;
    }

    unsigned char buffer[8];
    size_t n;
    unsigned int offset = 0;

    while ((n = fread(buffer, 1, 8, fp)) > 0) {
        printf("%08X: ", offset);

        for (size_t i = 0; i < 8; i++) {
            if (i < n) {
                printf("%02X ", buffer[i]);
            } else {
                printf("   ");
            }
        }

        printf("|");
        for (size_t i = 0; i < n; i++) {
            if (isprint(buffer[i])) {
                printf("%c", buffer[i]);
            } else {
                printf(".");
            }
        }
        printf("|\n");

        offset += (unsigned int)n;
    }

    fclose(fp);
    return 0;
}

解説

この問題の中心は、1バイト単位での観察isprint による文字判定です。
ファイルダンプはエンディアンや構造体の並びを確認するうえでも役立ちます。
Person 型や Book 型のファイルをダンプしてみると、数値部分と文字列部分がどのように並んでいるかを観察できます。

17章の確認問題

次の項目について、正しいものには〇、間違っているものには×をつけてください。

① fopen でファイルを開くとき、失敗する可能性があるので返却値を確認する必要がある。
② fopen の mode に a を指定すると、既存ファイルの先頭から上書きして書き込まれる。
③ fgets は、改行文字を含めて読み込むことがある。
④ fputc は1文字ずつ書き込む関数であり、fputs は文字列を書き込む関数である。
⑤ fscanf は、書式文字列とファイル内容の形式がずれていても必ず正しく読み込める。
⑥ バイナリファイルでは、構造体や配列の内容をそのまま保存しやすい。
⑦ バイナリファイルは常にテキストファイルより人が読みやすい。
⑧ rb や wb の b は、バイナリモードで開くことを表す。
⑨ fread と fwrite の返却値は、基本的に読み書きした要素数として考える。
⑩ 異なる環境間でバイナリデータをやり取りするときは、エンディアンの違いに注意が必要なことがある。

解答と解説

① 〇
ファイルオープンは、ファイルが存在しない、権限がない、パスが違うなどの理由で失敗する可能性があります。そのため、fopen の返却値が NULL かどうかを確認するのが基本です。これは元の確認問題でも強調されていた考え方です。

② ×
a は追加モードです。既存ファイルの末尾に追記します。先頭から上書きしたいなら w 系のモードになります。

③ 〇
fgets は1行読み込みに便利ですが、改行文字も配列に入ることがあります。そこが文字列処理で少し注意したい点です。

④ 〇
fputc は1文字単位、fputs は文字列単位です。1文字ずつの細かな処理と、文字列をまとめて扱う処理の使い分けができます。

⑤ ×
fscanf は、書式と入力データの並びが合っていないと正しく読み込めない可能性があります。元文書でも、書式文字列に合致しない入力では想定外の動作になると説明されていました。

⑥ 〇
バイナリファイルでは、構造体や数値配列をそのまま fwrite し、fread で読み戻しやすいのが特徴です。元の実践問題群も、その性質を活かした構成でした。

⑦ ×
バイナリファイルは人が直接読みにくい形式です。人が読みやすいのはテキストファイルです。用途によって使い分けることが大切だという説明は、元の確認問題にも含まれていました。

⑧ 〇
rb や wb の b は、バイナリモードを表します。特に Windows では、テキストモードとバイナリモードの違いを意識することが重要です。

⑨ 〇
fread と fwrite は、成功時に「バイト数」ではなく「要素数」を返します。ここは間違えやすいポイントで、元の確認問題でも×の選択肢として出されていました。

⑩ 〇
異なる環境間でバイナリデータをやり取りするときは、エンディアンの違いで数値の解釈が変わることがあります。ダンプ表示の問題ともつながる重要な注意点です。元のダンプ問題でも、バイトの並び順が逆に見える理由としてエンディアンに触れられていました。

実践問題を解くときのコツ

最後に、バイナリファイルの問題を解くときに意識すると進めやすいポイントを整理しておきます。

コツ理由
まず保存単位を決める配列全体か、構造体1件かで fwrite と fread の形が変わる
モードを先に決めるwb、ab、rb のどれかを間違えると動きが変わる
返却値を確認する失敗時の切り分けがしやすい
表示用の printf を入れる読み込めた内容を確認しやすい
条件判定は関数化するmain が読みやすくなる
必要に応じてダンプするバイト列の確認ができる

バイナリファイルは最初は少し見えにくいテーマですが、実践問題を通して「何を、どの単位で、どう保存し、どう戻すか」がわかってくると、一気に使いやすくなります。