6日でできる 新C言語入門|C言語のバイナリファイル操作入門

見えないデータの流れをつかめば、C言語のファイル操作はもっと強くなる。
バイト単位で扱うバイナリファイルを学んで、実用的な入出力処理へ一歩進みましょう。

C言語でファイル操作を学ぶとき、最初に触れやすいのはテキストファイルです。
ただ、実際のプログラム開発では、文字として読めるデータだけを扱うとは限りません。画像データ、設定データ、構造体の保存データ、実行形式に近いデータなど、バイト列そのものをそのまま保存したい場面はとても多くあります。

そんなときに活躍するのが、バイナリファイルの読み書きです。
バイナリファイルでは、データを文字として解釈するのではなく、メモリ上の値をそのままバイト単位で扱います。そのため、効率よく保存できるだけでなく、数値や構造体の内容を扱いやすいという大きな利点があります。

この記事では、バイナリファイルの基本的な考え方から、fwrite と fread を使った基本操作、fseek と ftell を使ったファイルサイズの取得、動的メモリを組み合わせた読み込み方法まで、順番に丁寧に見ていきます。
最初は少し硬い印象があるかもしれませんが、流れが分かるととても整理しやすい分野です。ひとつずつ確認しながら進めていきましょう。

バイナリファイルとは何か

バイナリファイルは、データを文字列としてではなく、バイトの並びとして保存するファイルです。
テキストファイルでは、たとえば数字の 123 を保存するとき、文字の 1、2、3 として記録されます。
一方、バイナリファイルでは、数値や配列、構造体の内容を、そのままメモリ上の表現に近い形で保存できます。

テキストファイルとバイナリファイルの違いを表にすると、次のようになります。

項目テキストファイルバイナリファイル
保存単位文字バイト
人が直接読めるか読みやすい基本的に読めない
主な用途ログ、設定、文章データ画像、音声、構造体、独自データ形式
よく使う関数fprintf、fgets、fgetcfwrite、fread
開くモードr、w、arb、wb、ab

バイナリファイルは中身を目で読みにくい反面、無駄なくデータを保存できます。
特に、整数配列や構造体の配列などをそのまま保存したいときは、とても便利です。

図:バイナリファイル操作の基本の流れ

この図から分かること

この図から分かるのは、バイナリファイル操作も基本の流れはとてもシンプルだということです。
まず fopen でファイルを開き、そのあと fwrite で書き込むか、fread で読み込みます。そして最後に fclose でファイルを閉じます。

テキストファイルとの大きな違いは、やり取りする中身が文字列ではなく、バイト列だという点です。
この流れを最初にしっかり押さえておくと、後で出てくる応用操作も理解しやすくなります。

バイナリファイルを開くモード

バイナリファイルでは、ファイルを開くときのモードが重要です。
代表的なモードは次のとおりです。

モード意味
rbバイナリ読み込み
wbバイナリ書き込み
abバイナリ追記
rb+バイナリ読み書き
wb+バイナリ書き込みと読み込み
ab+バイナリ追記と読み込み

ここで大切なのは、バイナリファイルでは b が付くということです。
テキストとしてではなく、バイト列として扱う意思を明確にするために使います。

たとえば、ファイルへ新しくバイナリデータを書き込みたいときは wb を使います。
既存ファイルをバイナリとして読み込みたいときは rb を使います。

fwrite と fread の役割

バイナリファイルの基本操作では、fwrite と fread が中心になります。

関数役割
fwriteメモリ上のデータをファイルへ書き込む
freadファイル上のデータをメモリへ読み込む
fcloseファイルを閉じる

書式も確認しておきましょう。

関数書式
fwritefwrite(データの先頭アドレス, 1要素のサイズ, 要素数, ファイルポインタ)
freadfread(読み込み先アドレス, 1要素のサイズ, 要素数, ファイルポインタ)

ここでのポイントは、1要素のサイズ と 要素数 を分けて指定することです。
たとえば unsigned char 型の配列を 4 バイト書き込むなら、1要素のサイズは sizeof(unsigned char)、要素数は 4 になります。

バイナリファイルへ書き込み、読み戻して確認する

まずは一番基本となる、バイナリデータの書き込みと読み込みを確認してみましょう。
次のプログラムでは、5バイト分のデータを testdata.bin に保存し、そのあと同じファイルを読み戻して内容を16進数で表示します。

プロジェクト/ファイル名: Lesson68_1/main.c

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

int main(void) {
    FILE* fp;
    unsigned char wdata[] = { 0x12, 0x34, 0x56, 0xA0, 0xFF };
    unsigned char rdata[5];
    size_t written;
    size_t read_count;
    int i;

    /* バイナリ書き込みモードでファイルを開く */
    fp = fopen("testdata.bin", "wb");
    if (fp == NULL) {
        printf("ファイルの書き込みオープンに失敗しました。\n");
        return 1;
    }

    /* 配列の内容をバイナリファイルへ書き込む */
    written = fwrite(wdata, sizeof(unsigned char), 5, fp);
    if (written != 5) {
        printf("ファイルへの書き込みに失敗しました。\n");
        fclose(fp);
        return 1;
    }

    fclose(fp);

    /* バイナリ読み込みモードでファイルを開く */
    fp = fopen("testdata.bin", "rb");
    if (fp == NULL) {
        printf("ファイルの読み込みオープンに失敗しました。\n");
        return 1;
    }

    /* ファイルから5バイト読み込む */
    read_count = fread(rdata, sizeof(unsigned char), 5, fp);
    if (read_count != 5) {
        printf("ファイルの読み込みに失敗しました。\n");
        fclose(fp);
        return 1;
    }

    fclose(fp);

    /* 読み込んだ内容を16進数で表示する */
    printf("読み込んだデータ:");
    for (i = 0; i < 5; i++) {
        printf("%02X ", rdata[i]);
    }
    printf("\n");

    return 0;
}

実行結果

読み込んだデータ:12 34 56 A0 FF

このプログラムでは、まず wdata という unsigned char 配列を用意しています。

unsigned char wdata[] = { 0x12, 0x34, 0x56, 0xA0, 0xFF };

これは 1 バイト単位のデータ列です。
バイナリファイルの学習では、unsigned char 配列を使うと、1バイトごとの動きが見やすくなるのでとても分かりやすいです。

そのあと、fopen で testdata.bin を wb モードで開きます。

fp = fopen("testdata.bin", "wb");

続いて fwrite を使って、wdata の内容をそのままファイルへ書き込んでいます。

written = fwrite(wdata, sizeof(unsigned char), 5, fp);

ここでは、unsigned char のサイズは 1 バイトなので、5 要素ぶん、つまり 5 バイトを書き込んでいます。

その後、同じファイルを rb モードで開き、fread を使って rdata 配列へ読み戻しています。

read_count = fread(rdata, sizeof(unsigned char), 5, fp);

最後に、読み込んだ内容を 16進数で表示しています。
%02X を使うことで、1バイトの値を 2桁の16進数で見やすく表示できます。

fwrite と fread は戻り値も確認する

fwrite と fread を使うときは、戻り値を確認する癖をつけると安全です。
どちらも、実際に処理できた要素数を返します。

関数戻り値の意味
fwrite実際に書き込めた要素数
fread実際に読み込めた要素数

期待した要素数と一致しなければ、途中で失敗した可能性があります。
そのため、次のようにチェックするのが安心です。

if (written != 5) {
    printf("ファイルへの書き込みに失敗しました。\n");
}

学習の初期段階では、省略されることもありますが、実際のプログラムではとても大切な確認です。

図:fwrite と fread のデータの流れ

この図から分かること

この図から分かるのは、fwrite と fread は、メモリとファイルのあいだでバイト列をやり取りする関数だということです。
fwrite はメモリ上の配列や構造体の内容をファイルへ保存し、fread はファイル上のバイナリデータをメモリへ読み込みます。

文字列として変換しているわけではなく、データの並びをそのまま扱っているところが、テキストファイル操作との大きな違いです。

ファイルサイズが分からないときの考え方

バイナリファイルでは、読み込むファイルの大きさが事前に分からないことがよくあります。
固定長の配列なら簡単ですが、サイズ不明のファイルを読むには、まずファイルサイズを調べ、そのサイズぶんのメモリを用意する必要があります。

そんなときに使うのが、fseek と ftell です。

関数役割
fseekファイル位置を移動する
ftell現在のファイル位置を取得する

基本の流れは次のとおりです。

  1. fseek(fp, 0, SEEK_END) でファイル末尾へ移動する
  2. ftell(fp) で現在位置を取得する
  3. その値がファイルサイズになる
  4. fseek(fp, 0, SEEK_SET) で先頭へ戻る
  5. malloc で必要なサイズのメモリを確保する
  6. fread で一気に読み込む

この流れは、サイズ不明のバイナリファイルを扱うときの定番パターンです。

ファイルサイズを調べて動的に読み込む

次のプログラムでは、testdata.bin のサイズを調べ、そのサイズぶんのメモリを動的に確保して内容を読み込みます。

プロジェクト/ファイル名: Lesson68_2/main.c

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

int main(void) {
    FILE* fp;
    long filesize;
    unsigned char* buffer;
    size_t read_count;
    long i;

    /* バイナリ読み込みモードでファイルを開く */
    fp = fopen("testdata.bin", "rb");
    if (fp == NULL) {
        printf("ファイルオープンに失敗しました。\n");
        return 1;
    }

    /* ファイル末尾へ移動してサイズを調べる */
    fseek(fp, 0, SEEK_END);
    filesize = ftell(fp);

    /* 読み込み位置をファイル先頭へ戻す */
    fseek(fp, 0, SEEK_SET);

    /* ファイルサイズぶんのメモリを確保する */
    buffer = (unsigned char*)malloc(filesize);
    if (buffer == NULL) {
        printf("メモリ確保に失敗しました。\n");
        fclose(fp);
        return 1;
    }

    /* ファイル全体を一度に読み込む */
    read_count = fread(buffer, 1, filesize, fp);
    if (read_count != (size_t)filesize) {
        printf("ファイルの読み込みに失敗しました。\n");
        free(buffer);
        fclose(fp);
        return 1;
    }

    fclose(fp);

    /* 読み込んだ内容を16進数で表示する */
    printf("ファイルサイズ:%ldバイト\n", filesize);
    printf("読み込んだデータ:");
    for (i = 0; i < filesize; i++) {
        printf("%02X ", buffer[i]);
    }
    printf("\n");

    free(buffer);

    return 0;
}

実行結果

ファイルサイズ:5バイト
読み込んだデータ:12 34 56 A0 FF

このプログラムで最初に注目したいのは、ファイルサイズの調べ方です。

fseek(fp, 0, SEEK_END);
filesize = ftell(fp);
fseek(fp, 0, SEEK_SET);

まず SEEK_END を使って末尾へ移動し、そのときの位置を ftell で取得しています。
ファイル先頭から末尾までのバイト数が、そのままファイルサイズになります。

そのあと SEEK_SET を使って、読み込み位置を先頭へ戻しています。
これを忘れると、読み込み開始位置が末尾のままになってしまうので注意が必要です。

次に、malloc でファイルサイズぶんのメモリを確保しています。

buffer = (unsigned char*)malloc(filesize);

これで、どれだけの大きさのファイルでも、そのサイズに合わせたバッファを用意できます。

そして fread でファイル全体を一括で読み込みます。

read_count = fread(buffer, 1, filesize, fp);

ここでは 1 バイトを filesize 個読み込む指定になっています。
こう書くと、ファイル全体をバイト単位で扱っていることが分かりやすいです。

fseek と ftell をもう少し丁寧に理解する

fseek は、ファイル内の現在位置を移動する関数です。
移動の基準になる定数として、SEEK_SET、SEEK_CUR、SEEK_END を使います。

定数意味
SEEK_SETファイル先頭を基準にする
SEEK_CUR現在位置を基準にする
SEEK_ENDファイル末尾を基準にする

たとえば、

fseek(fp, 0, SEEK_END);

は、ファイル末尾から 0 バイト移動する、つまり末尾そのものへ移動するという意味です。

一方、ftell は現在位置を返します。

filesize = ftell(fp);

末尾へ移動した直後に呼び出せば、先頭から末尾までの距離、つまりファイルサイズが得られます。

図:fseek と ftell でファイルサイズを調べる流れ

この図から分かること

この図から分かるのは、サイズ不明のバイナリファイルは、いきなり読むのではなく、まずサイズを把握してから読み込むのが基本だということです。
fseek で末尾へ移動し、ftell で位置を取得すると、ファイルサイズが分かります。そのあと先頭へ戻してから fread で読み込む流れになります。

この方法を覚えておくと、可変長のファイルや、サイズが実行時まで分からないデータも扱いやすくなります。

バイナリファイル操作で意識したい注意点

バイナリファイルは便利ですが、いくつか注意点もあります。
特に学習中のうちに意識しておくと、後でつまずきにくくなります。

fopen の失敗を必ず確認する

ファイルを開けなければ、読み書きはできません。
fopen の戻り値が NULL かどうかを毎回確認しましょう。

fwrite と fread の戻り値を確認する

途中で読み書きに失敗しても、戻り値を見ないと気づけないことがあります。
期待した要素数と一致しているかを確認するのが安全です。

malloc で確保したメモリは free する

サイズ不明のファイルを読むときは、動的メモリを使うことが多いです。
使い終わったら free で解放することを忘れないようにしましょう。

テキストファイルのように中身を目で読めない

バイナリファイルは内容が読みにくいため、デバッグのときは 16進数表示を使うと便利です。
今回のように %02X で出力すると、1バイトごとの状態が確認しやすくなります。

構造体をそのまま保存するときは環境差に注意する

バイナリファイルでは、構造体をそのまま fwrite で保存することもできます。
ただし、その場合はメモリ配置、アライメント、型サイズ、エンディアンなどの影響を受けることがあります。

同じ環境で使う用途では便利ですが、異なる環境間で完全に共通化したい場合は、保存形式を明確に設計することが大切です。

よく使う関数を整理しておこう

最後に、今回登場した関数を一覧で整理しておきます。

関数主な役割ポイント
fopenファイルを開くバイナリでは rb、wb などを使う
fwriteバイナリデータを書き込む要素サイズと要素数を指定する
freadバイナリデータを読み込む戻り値で実際に読めた数を確認する
fseek読み書き位置を移動するSEEK_SET、SEEK_CUR、SEEK_END を使う
ftell現在位置を取得する末尾で呼ぶとファイルサイズが分かる
fcloseファイルを閉じる開いたら必ず閉じる
malloc必要サイズのメモリを確保するサイズ不明ファイルの読み込みで便利
free確保したメモリを解放する解放忘れに注意する

バイナリファイル操作を身につけるとできること

バイナリファイル操作を理解すると、C言語で扱えるデータの幅が一気に広がります。
単なる文字列の入出力だけでなく、配列データ、数値データ、構造体データ、さらには独自フォーマットのファイルまで扱えるようになります。

特に、次のような場面で役立ちます。

活用例内容
設定データの保存数値やフラグを効率よく記録できる
配列データの保存センサー値や測定値をそのまま保存しやすい
構造体データの保存複数項目をひとまとまりで扱える
画像や音声の下処理バイト列としてデータを扱える
独自ファイル形式自分で設計したデータ形式を実装できる

ここまでの内容をしっかり押さえれば、バイナリファイルの基本はかなり見えてきます。
まずは Lesson68_1 でバイナリの書き込みと読み戻しを確認し、次に Lesson68_2 でサイズ不明ファイルの読み込みの流れを体験してみてください。
実際に手を動かしてみると、fwrite、fread、fseek、ftell の役割がぐっと実感しやすくなります。