バイナリファイルは、整数や浮動小数点数、構造体などをそのままのバイト列で保存します。
テキストと比べて高速で正確に保存できる一方、中身は人間には読めません。
本記事では、C言語のfwriteとfreadを使って、バイナリファイルを安全に読み書きする方法を、初心者向けに丁寧に解説します。
バイナリファイル入門
バイナリファイルとは
バイナリファイルは、メモリ中のデータをそのままのバイト列として保存したファイルです。
整数や浮動小数点数、構造体などの情報が、文字に変換されずに記録されます。
たとえばintの値1つは通常4バイトで、その4バイトがそのままファイルに書き込まれます。
テキストとの違い
テキストファイルは、人が読める文字列としてデータを保存します。
バイナリファイルは、人が読める形にはなっていませんが、変換の手間がなく高速でファイルサイズも小さくなりやすいという特徴があります。
以下は、数値1234を例にした違いのイメージです。
- テキスト保存: 文字
'1''2''3''4'の4バイト以上を保存 - バイナリ保存: 1234を表す4バイト(例: リトルエンディアン環境で
D2 04 00 00)
バイナリは環境依存の要素(エンディアン、整数や浮動小数点の表現、構造体のパディングなど)があります。
同じプログラムと環境で読み書きするのが基本です。
異なる環境間でやり取りする場合は、固定幅整数uint32_tなどの採用や、独自のフォーマット定義が必要です。
fopenのモード
バイナリファイルを開くときは、必ずbを付けたモードを使います。
特にWindowsではbがないと改行変換が入るため、バイナリが壊れます。
以下に主なモードをまとめます。
| モード | 目的 | 挙動 |
|---|---|---|
| rb | 読み取り | 既存ファイルを読み取り専用で開く |
| wb | 書き込み | 新規作成またはファイルを空にして書き込み |
| ab | 追記 | 既存の末尾にだけ書き込み。なければ新規作成 |
| rb+ | 読み書き | 既存を読み書き。位置は先頭 |
| wb+ | 読み書き | 新規作成または空にして読み書き |
| ab+ | 読み書き | 読み書き可能だが、書き込みは常に末尾 |
モードにbがないと意図しない改行変換が起きるため、バイナリでは必ず<b>rb, wb, ab</b>系を使ってください。
FILE*とfcloseの基本
fopenはファイルを開いてFILE*というハンドルを返します。
エラー時はNULLが返るため、必ずチェックします。
使い終わったらfcloseでクローズします。
クローズ忘れは、書き込みが反映されない、ファイル数制限に達する、といった不具合につながります。
// open_close.c
#include <stdio.h>
int main(void) {
FILE *fp = fopen("sample.bin", "wb"); // バイナリ書き込み
if (!fp) {
perror("fopen"); // エラーの理由を表示
return 1;
}
// ... ここで書き込み処理などをする ...
if (fclose(fp) != 0) { // 正常に閉じられたか確認
perror("fclose");
return 1;
}
return 0;
}
このコードは出力を行いません。
fwriteの使い方
基本の書き方と引数
プロトタイプ
fwriteの宣言は次のとおりです。
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
引数の意味
ptr: 書き込みたいデータの先頭アドレスsize: 1要素のバイト数(例:sizeof(int))count: 要素数(例: 配列の個数)stream: 書き込み先のFILE*
配列を書き込むときはsizeに要素1つのサイズ、countに要素数を指定します。
最小の例
// fwrite_basic.c
#include <stdio.h>
int main(void) {
int numbers[3] = {10, 20, 30};
FILE *fp = fopen("data.bin", "wb"); // 既存があれば空にして書き込み
if (!fp) {
perror("fopen");
return 1;
}
// intを3個、まとめて書き込む
size_t written = fwrite(numbers, sizeof(int), 3, fp);
if (written != 3) {
// 部分書き込みやエラー
perror("fwrite");
fclose(fp);
return 1;
}
fclose(fp);
printf("3個のintを書き込みました\n");
return 0;
}
3個のintを書き込みました
戻り値で書き込み数を確認
fwriteは成功した要素数を返します。
要求したcountより小さい値が返ることがあります。
必ず確認し、エラーや部分書き込みに対処します。
// fwrite_check_return.c
#include <stdio.h>
int main(void) {
double values[4] = {1.5, 2.5, 3.5, 4.5};
FILE *fp = fopen("values.bin", "wb");
if (!fp) {
perror("fopen");
return 1;
}
size_t want = 4;
size_t n = fwrite(values, sizeof(double), want, fp);
if (n < want) {
// 書き込めなかった要素がある場合
if (ferror(fp)) {
perror("fwrite"); // 実際のエラー原因を表示
} else {
fprintf(stderr, "部分書き込みが発生しました(%zu/%zu)\n", n, want);
}
fclose(fp);
return 1;
}
fclose(fp);
printf("doubleを%zu個、正常に書き込みました\n", n);
return 0;
}
doubleを4個、正常に書き込みました
配列を書き込む例
配列の書き込みでは、size=sizeof(要素型)、count=配列の要素数を指定します。
// fwrite_array.c
#include <stdio.h>
int main(void) {
// 成績データを仮にintで用意
int scores[] = {85, 92, 76, 100, 67};
size_t count = sizeof(scores) / sizeof(scores[0]);
FILE *fp = fopen("scores.bin", "wb");
if (!fp) {
perror("fopen");
return 1;
}
size_t n = fwrite(scores, sizeof(scores[0]), count, fp);
if (n != count) {
perror("fwrite");
fclose(fp);
return 1;
}
fclose(fp);
printf("scores.binに%dバイト書き込みました\n", (int)(count * sizeof(scores[0])));
return 0;
}
scores.binに20バイト書き込みました
追記と上書きの違い
wbはファイルを空にして書き込み、abは末尾に追記します。
違いは次のデモで明確です。
// append_vs_overwrite.c
#include <stdio.h>
#include <stdlib.h>
static void print_file(const char *path) {
FILE *fp = fopen(path, "rb");
if (!fp) { perror("fopen"); return; }
int x;
printf("%sの内容: ", path);
while (fread(&x, sizeof(x), 1, fp) == 1) {
printf("%d ", x);
}
if (ferror(fp)) perror("fread");
printf("\n");
fclose(fp);
}
int main(void) {
const char *path = "append_demo.bin";
// 1. 上書きモード(wb): 2個書く
{
FILE *fp = fopen(path, "wb");
if (!fp) { perror("fopen"); return 1; }
int a[] = {1, 2};
fwrite(a, sizeof(int), 2, fp);
fclose(fp);
}
print_file(path); // => 1 2
// 2. 追記モード(ab): 2個追加
{
FILE *fp = fopen(path, "ab");
if (!fp) { perror("fopen"); return 1; }
int b[] = {3, 4};
fwrite(b, sizeof(int), 2, fp);
fclose(fp);
}
print_file(path); // => 1 2 3 4
// 3. 再び上書き(wb): 1個だけで上書き
{
FILE *fp = fopen(path, "wb");
if (!fp) { perror("fopen"); return 1; }
int c = 99;
fwrite(&c, sizeof(int), 1, fp);
fclose(fp);
}
print_file(path); // => 99
return 0;
}
append_demo.binの内容: 1 2
append_demo.binの内容: 1 2 3 4
append_demo.binの内容: 99
freadの使い方
基本の読み方と引数
プロトタイプ
freadの宣言は次のとおりです。
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
引数の意味
ptr: 読み取り先バッファの先頭アドレスsize: 1要素のバイト数count: 読み取りたい要素数stream: 読み取り元のFILE*
最小の例
// fread_basic.c
#include <stdio.h>
int main(void) {
int numbers[3] = {0};
FILE *fp = fopen("data.bin", "rb"); // fwrite_basic.cが作ったファイルを読む
if (!fp) {
perror("fopen");
return 1;
}
size_t n = fread(numbers, sizeof(int), 3, fp);
if (n != 3) {
if (feof(fp)) {
fprintf(stderr, "EOFに達しました。読み取れた要素数: %zu\n", n);
} else if (ferror(fp)) {
perror("fread");
}
fclose(fp);
return 1;
}
fclose(fp);
printf("読み取った値: %d %d %d\n", numbers[0], numbers[1], numbers[2]);
return 0;
}
読み取った値: 10 20 30
戻り値で読み取り数を確認
freadは実際に読み取れた要素数を返します。
要求したcountに満たない場合は、EOFかエラーの可能性があります。
feofとferrorで原因を切り分けます。
// fread_check_return.c
#include <stdio.h>
int main(void) {
FILE *fp = fopen("scores.bin", "rb"); // fwrite_array.cで作成
if (!fp) {
perror("fopen");
return 1;
}
int v;
size_t total = 0;
while (1) {
size_t n = fread(&v, sizeof(v), 1, fp); // 1要素ずつ読む
if (n == 1) {
printf("値: %d\n", v);
total += n;
} else { // n == 0
if (feof(fp)) {
printf("EOFに達しました。合計%zu個読み取りました。\n", total);
} else if (ferror(fp)) {
perror("fread");
}
break;
}
}
fclose(fp);
return 0;
}
値: 85
値: 92
値: 76
値: 100
値: 67
EOFに達しました。合計5個読み取りました。
EOFで終わりを判断
EOFは「読み取りを試みた結果、これ以上読めない」と判明した後に検出できます。
feofは読む前に真にならないことに注意してください。
次のようなパターンが定番です。
// fread_eof_pattern.c
#include <stdio.h>
int main(void) {
FILE *fp = fopen("scores.bin", "rb");
if (!fp) {
perror("fopen");
return 1;
}
int buf[2]; // 2個ずつまとめて読む(ブロック読み)
size_t total = 0;
while (1) {
size_t n = fread(buf, sizeof(buf[0]), 2, fp); // 最大2個
if (n > 0) {
for (size_t i = 0; i < n; ++i) {
printf("%d ", buf[i]);
}
total += n;
}
if (n < 2) { // 要求数未満ならEOFまたはエラー
if (feof(fp)) {
printf("\nEOFに達しました。合計%zu個\n", total);
} else if (ferror(fp)) {
perror("fread");
}
break;
}
}
fclose(fp);
return 0;
}
85 92 76 100 67
EOFに達しました。合計5個
配列を読み込む例
ここでは構造体の配列をファイルに書いて、後から読み戻す例を示します。
構造体のバイナリはパディングやエンディアンの影響を受ける可能性があるため、同じ環境での読み書きを前提にします。
// struct_rw.c
#include <stdio.h>
#include <stdint.h>
#include <string.h>
typedef struct {
uint32_t id; // 4バイトの符号なし整数
double score; // 8バイトの倍精度実数
char name[16]; // 固定長の名前(終端0が入るように注意)
} Record;
static void write_records(const char *path) {
FILE *fp = fopen(path, "wb");
if (!fp) { perror("fopen"); return; }
Record recs[2];
recs[0].id = 1; recs[0].score = 98.5; strncpy(recs[0].name, "ALPHA", sizeof(recs[0].name));
recs[1].id = 2; recs[1].score = 76.25; strncpy(recs[1].name, "BETA", sizeof(recs[1].name));
size_t n = fwrite(recs, sizeof(Record), 2, fp);
if (n != 2) perror("fwrite");
fclose(fp);
}
static void read_records(const char *path) {
FILE *fp = fopen(path, "rb");
if (!fp) { perror("fopen"); return; }
Record rec;
while (fread(&rec, sizeof(rec), 1, fp) == 1) {
printf("id=%u score=%.2f name=%s\n", rec.id, rec.score, rec.name);
}
if (ferror(fp)) perror("fread");
fclose(fp);
}
int main(void) {
const char *path = "records.bin";
write_records(path);
read_records(path);
return 0;
}
id=1 score=98.50 name=ALPHA
id=2 score=76.25 name=BETA
初心者が知っておきたい注意点
bモードを付けるのを忘れない
Windowsではrbやwbの<b>b</b>が必須です。
これがないと改行の自動変換などが発生し、バイナリデータが壊れます。
UNIX系では<b>b</b>の有無は同じ動作になることが多いですが、常に付けると覚えておくと安全です。
sizeとcountの指定ミスに注意
sizeは1要素のサイズ、countは要素数です。
例えばint a[100]なら、sizeof(a[0])と100を指定します。
よくある間違いはsize=sizeof(a)、count=1のように配列全体のサイズをsizeに入れてしまうことです。
複数要素をまとめて扱う利点を活かすには、1要素のサイズ×要素数という考え方を徹底します。
表現としては次の書き方が安全です。
| 目的 | 指定 |
|---|---|
| 配列aを丸ごと | fwrite(a, sizeof a[0], sizeof a / sizeof a[0], fp) |
| 配列aのうちn個 | fwrite(a, sizeof a[0], n, fp) |
読み書きは必要な回数だけ繰り返す
freadとfwriteは、要求した数だけ常に処理できるとは限りません。
特に大きなデータやパイプ、ソケットでは部分読み書きが起きやすいです。
戻り値で実績数を確認し、足りない分はループで繰り返すのが堅牢です。
// robust_rw.c
#include <stdio.h>
#include <string.h>
// 例: バッファbufをsizeバイト、確実に書き切る関数
size_t write_all(const void *buf, size_t size, FILE *fp) {
const unsigned char *p = (const unsigned char *)buf;
size_t written = 0;
while (written < size) {
size_t n = fwrite(p + written, 1, size - written, fp);
if (n == 0) {
if (ferror(fp)) return written; // エラー発生
break; // 進展なし(通常は何らかの問題)
}
written += n;
}
return written;
}
// 例: sizeバイトを読み切る(EOFなら途中で終了)
size_t read_all(void *buf, size_t size, FILE *fp) {
unsigned char *p = (unsigned char *)buf;
size_t readn = 0;
while (readn < size) {
size_t n = fread(p + readn, 1, size - readn, fp);
if (n == 0) {
// EOFやエラー
break;
}
readn += n;
}
return readn;
}
int main(void) {
char msg[] = "Hello Binary";
FILE *fp = fopen("robust.bin", "wb");
if (!fp) { perror("fopen"); return 1; }
size_t w = write_all(msg, strlen(msg), fp);
fclose(fp);
printf("書いたバイト数: %zu\n", w);
char buf[32] = {0};
fp = fopen("robust.bin", "rb");
if (!fp) { perror("fopen"); return 1; }
size_t r = read_all(buf, sizeof(msg) - 1, fp);
fclose(fp);
printf("読んだ内容: %s (%zuバイト)\n", buf, r);
return 0;
}
書いたバイト数: 12
読んだ内容: Hello Binary (12バイト)
ferrorでエラーを確認
freadやfwriteの戻り値だけでは、EOFとエラーを区別できない場合があります。
feofとferrorを使い分けることで、原因が明確になります。
必要に応じてperrorでエラー詳細を表示しましょう。
// ferror_example.c
#include <stdio.h>
int main(void) {
FILE *fp = fopen("non_exist.bin", "rb");
if (!fp) {
perror("fopen"); // そもそも開けなければ以降は実行されない
return 1;
}
int x;
size_t n = fread(&x, sizeof(x), 1, fp);
if (n != 1) {
if (feof(fp)) {
fprintf(stderr, "EOFでした\n");
} else if (ferror(fp)) {
perror("fread");
} else {
fprintf(stderr, "不明な理由で読み取り失敗\n");
}
}
fclose(fp);
return 0;
}
このコードは、ファイルが存在しないためfopenで失敗し、perrorが原因を表示します。
補足:
- 異なる実行環境間でバイナリをやり取りする場合は、固定幅整数
uint32_tなどを使い、エンディアンを揃えるか明示的に変換します。 - 構造体はアライメントやパディングが入るため、構造体のそのまま保存は移植性が低いです。フィールドごとに書く、既知のフォーマットを使う、JSONやProtocol Buffersなどのシリアライズを検討する、といった方法もあります。
まとめ
本記事では、C言語でfwriteとfreadを使ってバイナリファイルを扱う基本を解説しました。
バイナリは高速で正確にデータを保存できる一方、環境依存や目視困難という側面があります。
安全に使うためには、以下を必ず守ると良いです。
まずモードにbを付ける、sizeとcountを正しく指定する、戻り値で実際の処理数を確認して必要ならループする、そしてfeofとferrorで原因を切り分けることです。
理解が進んだら、構造体の取り扱いや移植性、独自フォーマット設計などにも挑戦してみてください。
