C言語基礎|テキストファイルとバイナリファイル

「文字で保存する?ビットで保存する?―テキストとバイナリの違いを知ると、データ保存が一気に強くなる!」

これまで扱ってきたファイルは、見て読める“文字の並び”として保存されるテキストファイルが中心でした。テキストは人間にとって扱いやすい一方で、数値を保存すると誤差や桁数の問題が出やすいという弱点もあります。

一方、バイナリファイルはデータをメモリ上のビット列のまま保存します。人間が直接読めない代わりに、サイズが一定で、読み書きが速く、数値の精度も保持しやすいという強みがあります。

この節では、同じ数値を「テキストで保存した場合」と「バイナリで保存した場合」を比べながら、用途に応じた使い分けができるようになるのを目指します。

まず結論:テキストとバイナリの違い(超まとめ)

表:テキストファイルとバイナリファイルの違い

観点テキストファイルバイナリファイル
保存形式文字の並びビット列(メモリ表現に近い)
人間が読める?読める基本読めない
サイズ桁数や書式で増減型サイズで固定になりやすい
互換性高い(環境差に強い)低め(エンディアン、型サイズ、構造体詰め物など)
数値の精度書式次第で丸めが起きる基本そのまま保持できる
代表的な関数fprintf, fscanffwrite, fread

表の説明

  • テキストは「共有・編集・可視化」が得意。
  • バイナリは「高速・固定サイズ・精度維持」が得意。
  • どっちが正解ではなく、目的で選ぶのがポイントです。

サンプルプログラム

同じ double を「テキスト保存」と「バイナリ保存」で書いて、読み戻して差を確認します。

例:テキスト保存とバイナリ保存を比べるプログラム

プロジェクト名:chap13-10-1 ソースファイル名:chap13-10-1.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

int main(void)
{
    FILE *fp;
    double original = 1.0 / 3.0;   /* 誤差が出やすい値 */
    double x;

    printf("Original      : %.20f\n", original);

    /* --- テキスト保存(わざと桁を落とす) --- */
    fp = fopen("value.txt", "w");
    if (fp == NULL) {
        printf("ERROR: Cannot open value.txt\n");
        return 1;
    }
    /* 小数点以下6桁に丸めて書く -> 情報が落ちる */
    fprintf(fp, "%.6f\n", original);
    fclose(fp);

    /* テキストから読み戻す */
    x = 0.0;
    fp = fopen("value.txt", "r");
    if (fp == NULL) {
        printf("ERROR: Cannot open value.txt\n");
        return 1;
    }
    fscanf(fp, "%lf", &x);
    fclose(fp);

    printf("From text     : %.20f\n", x);
    printf("Diff (text)   : %.20f\n", x - original);

    /* --- バイナリ保存(ビット列をそのまま) --- */
    fp = fopen("value.bin", "wb");
    if (fp == NULL) {
        printf("ERROR: Cannot open value.bin\n");
        return 1;
    }
    fwrite(&original, sizeof(double), 1, fp);
    fclose(fp);

    /* バイナリから読み戻す */
    x = 0.0;
    fp = fopen("value.bin", "rb");
    if (fp == NULL) {
        printf("ERROR: Cannot open value.bin\n");
        return 1;
    }
    fread(&x, sizeof(double), 1, fp);
    fclose(fp);

    printf("From binary   : %.20f\n", x);
    printf("Diff (binary) : %.20f\n", x - original);

    printf("Done.\n");
    return 0;
}

これで「必ず誤差が見える」理由

  • テキスト保存は %.6f で 6 桁に丸めて書くので、必ず情報が削られます。
    例:0.3333333333… → 0.333333 に固定
  • 読み戻した値は 0.333333 から再構成されるので、original と一致しません。
  • その差を Diff (text) で 数値として表示するので、見逃しません

一方バイナリ保存は、original の double のビット列をそのまま保存・復元するので、通常は Diff (binary) が 0.00000000000000000000 に限りなく近い値になります(※表示上 0 になります)。

なぜテキスト保存で精度が落ちるの?

原因は大きく2つです。

図:テキスト保存で情報が落ちる流れ

double(内部は2進のビット列)
   ↓(fprintf で文字列化:桁数や丸めが入る)
"0.100000" のような文字列
   ↓(fscanf で数値化:再び2進に変換)
double(元のビット列と一致しないことがある)

図の説明

  • fprintf は “文字列としての表現” に変換します。ここで桁数が足りないと、元の情報が削られます。
  • いったん削られた情報は、読み取り時に復元できません。

テキスト保存で誤差が増えやすいポイント

ポイント何が起こる?対策
書き込み時の桁数不足丸めで情報が消える%.17g など十分な桁で出す(doubleなら目安)
10進↔2進変換そもそもピッタリ表せない値がある“誤差は起きうる”と理解して扱う
数値ごとに文字数が変わるファイルサイズが一定にならない固定長が必要ならバイナリを検討

表の説明

  • doubleの“正確な往復”を狙うなら、テキストでは桁指定が重要になります。
  • ただし「人間が読むログ」ならテキストの方が圧倒的に便利です。

バイナリ保存は何が嬉しい?

図:バイナリ保存はビット列をそのまま保存

double のビット列(メモリ上)
   ↓ fwrite(sizeof(double) バイトをそのまま)
ファイルに固定サイズで保存
   ↓ fread(同じサイズで読み戻す)
同じビット列が復元されやすい

図の説明

  • fwrite/fread は「数値を文字列にしない」ので、書式による丸めが起きません。
  • その代わり、人間が開いても読めません。

テキストとバイナリのサイズ感(整数例で直感をつける)

「357 と 2057」を例に、表で整理します。

整数を保存したときのサイズの違い(イメージ)

テキストで保存バイナリで保存
3573文字 → 3バイト常に sizeof(int) バイト
20574文字 → 4バイト常に sizeof(int) バイト

表の説明

  • テキストは“桁数=サイズ”になりやすい。
  • バイナリは“型のサイズ=サイズ”になりやすい。
  • だから大量データでは、バイナリが有利になりやすいです。

バイナリは万能ではない(注意点)

バイナリは便利ですが、次の点に注意が必要です。

バイナリファイルの注意点

注意点何が困る?
エンディアン別CPUで読み方が変わるx86と一部組み込み
型サイズsizeof(int) が環境で違う32bit/64bit
構造体の詰め物struct のバイト並びが変わるpadding
可読性目視確認できないテキストエディタで読めない

表の説明

  • 「同じ環境・同じプログラム」で使うならバイナリは強いです。
  • 「他環境・他言語と共有」するなら、テキストか、バイナリでもフォーマット設計(エンディアン固定など)が必要です。

fwrite と fread の書式と引数の意味

ここはバイナリ入出力の核なので、表でガッチリ押さえます。

fwrite

項目内容
ヘッダ#include <stdio.h>
形式size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
意味ptr から size バイトの要素を nmemb 個、stream に書く
返り値書けた要素数(nmemb まで)

fread

項目内容
ヘッダ#include <stdio.h>
形式size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
意味stream から size バイトの要素を nmemb 個、ptr に読む
返り値読めた要素数(nmemb まで)

表の説明

  • size は「1要素のサイズ」、nmemb は「要素数」です。
  • 戻り値が nmemb より小さければ、ファイル終端やエラーの可能性があります。

典型パターン(単体変数と配列)

単体と配列の読み書き

対象書き込み読み取り
int 変数 xfwrite(&x, sizeof(int), 1, fp)fread(&x, sizeof(int), 1, fp)
double 変数 vfwrite(&v, sizeof(double), 1, fp)fread(&v, sizeof(double), 1, fp)
int 配列 a(n要素)fwrite(a, sizeof(int), n, fp)fread(a, sizeof(int), n, fp)

表の説明

  • 単体はアドレスが必要なので &x の形になります。
  • 配列名 a は先頭要素へのポインタとして使えるので a のままでOKです。

演習問題

演習13-11:double 配列10要素をバイナリ保存して読み戻す

double 型で要素数10の配列を作り、バイナリファイルに書き込み、読み戻して表示せよ。

解答例

プロジェクト名:chap13-10-1 ソースファイル名:chap13-10-1.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

int main(void)
{
    FILE *fp;
    double a[10];
    double b[10];
    int i;

    /* 配列に値を入れる */
    for (i = 0; i < 10; i++) {
        a[i] = i * 0.25;  /* 例として0.25刻み */
        b[i] = 0.0;
    }

    /* 書き込み */
    fp = fopen("arr.bin", "wb");
    if (fp == NULL) {
        printf("ERROR: Cannot open arr.bin\n");
        return 1;
    }
    fwrite(a, sizeof(double), 10, fp);
    fclose(fp);

    /* 読み取り */
    fp = fopen("arr.bin", "rb");
    if (fp == NULL) {
        printf("ERROR: Cannot open arr.bin\n");
        return 1;
    }
    fread(b, sizeof(double), 10, fp);
    fclose(fp);

    /* 確認表示 */
    printf("Read back:\n");
    for (i = 0; i < 10; i++) {
        printf("%2d: %.20f\n", i, b[i]);
    }

    printf("Done.\n");
    return 0;
}

解説

  • 配列は fwrite(a, sizeof(double), 10, fp) の形で一気に書けます。
  • 大量データほど、この“一括読み書き”が効いてきます。

演習13-12:前回実行時刻を struct tm としてバイナリ保存する

現在時刻を取得し、struct tm の内容をそのままバイナリファイルに書き込み、次回起動時に読み戻して表示せよ。

解答例

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

int main(void)
{
    FILE *fp;
    struct tm last;
    struct tm now;

    /* 前回データを読む */
    fp = fopen("lasttime.bin", "rb");
    if (fp == NULL) {
        printf("This is the first run.\n");
    } else {
        size_t n = fread(&last, sizeof(struct tm), 1, fp);
        fclose(fp);
        if (n == 1) {
            printf("Last run: %04d-%02d-%02d %02d:%02d:%02d\n",
                   last.tm_year + 1900, last.tm_mon + 1, last.tm_mday,
                   last.tm_hour, last.tm_min, last.tm_sec);
        } else {
            printf("WARNING: Failed to read previous data.\n");
        }
    }

    /* 今回時刻を取得して書く */
    time_t t = time(NULL);
    struct tm *p = localtime(&t);
    now = *p;  /* 値としてコピー */

    fp = fopen("lasttime.bin", "wb");
    if (fp == NULL) {
        printf("ERROR: Cannot write file.\n");
        return 1;
    }
    fwrite(&now, sizeof(struct tm), 1, fp);
    fclose(fp);

    printf("Saved current time.\n");
    return 0;
}

解説

  • struct tm をバイナリで保存すると、書式を考えずに一発で保存できて楽です。
  • ただし struct tm の中身やサイズは処理系依存の部分もあるので、他環境へ持ち出す用途には向きにくいです。