プログラムが終了すると、メモリ上に保持されていたデータは失われます。たとえば、構造体を用いて実装した連絡先管理アプリケーションでは、実行中に追加・削除した情報がプロセス終了とともに消滅し、次回起動時には再入力が必要になります。これを回避するには、データを永続化——すなわち、外部ストレージ(例:ディスク上のファイルやデータベース)に保存する必要があります。
ファイルの基本概念
プログラム設計において、「ファイル」は大きく二種類に分類されます。
- プログラムファイル:ソースコード(
.c)、オブジェクトファイル(.obj)、実行可能ファイル(.exe)など、開発者が作成・管理するコード関連のファイル。 - データファイル:プログラムの実行中に生成・読み込むユーザーデータを格納するファイル(例:
contacts.dat)。このファイルは、実行中のプロセスとやり取りされる「データの容器」として機能します。
ファイル識別子(ファイル名)は、パス、基本名、拡張子から構成され、たとえば /home/user/data.bin のように表記されます。
ファイル操作の基礎:オープンとクローズ
C標準ライブラリでは、fopen() でファイルを開き、fclose() で閉じます。ファイル操作の起点となるのは FILE* 型ポインタで、これは内部的にファイル状態(位置、モード、エラー状態など)を管理する構造体への参照です。
FILE* fp = fopen("log.txt", "a+");
if (fp == NULL) {
perror("ファイルオープン失敗");
return -1;
}
// … 操作 …
fclose(fp);
fp = NULL; // ダングリングポインタ防止
主なモード:
"r":既存テキストファイルを読み込み専用で開く(存在しない場合は失敗)"w":新規テキストファイルを作成し書き込み専用(既存なら上書き)"a":既存または新規ファイルへ末尾追記(読み込み不可)"rb","wb","ab":それぞれバイナリモード版"r+","w+","a+":読み書き両用(w+は新規作成/上書き)
順次アクセスによるデータの入出力
標準入出力ストリーム(stdin, stdout, stderr)と同様のインターフェースで、ファイルに対しても多様なI/O関数が提供されます。
| 用途 | 関数 | 説明 |
|---|---|---|
| 1文字単位の読み書き | fgetc(), fputc() |
ASCII値を返す。EOFで終了判定 |
| 文字列単位の読み書き | fgets(), fputs() |
fgets()は改行含む最大n−1文字+ヌル終端 |
| 書式付き入出力 | fscanf(), fprintf() |
printf()/scanf()のファイル版 |
| バイナリブロック転送 | fread(), fwrite() |
構造体や配列の生データを直接読み書き |
例:構造体のバイナリ保存と復元
typedef struct { char name[32]; int age; double salary; } Employee;
Employee emp = {"Tanaka", 35, 7200000.0};
// 書き出し(バイナリ)
FILE* out = fopen("emp.bin", "wb");
fwrite(&emp, sizeof(Employee), 1, out);
fclose(out);
// 読み込み(バイナリ)
Employee loaded;
FILE* in = fopen("emp.bin", "rb");
size_t n = fread(&loaded, sizeof(Employee), 1, in);
if (n != 1) { /* エラー処理 */ }
fclose(in);
ランダムアクセス:位置制御
ファイル内の任意位置へ移動するには、fseek() を使います。基準点として、SEEK_SET(先頭)、SEEK_CUR(現在位置)、SEEK_END(末尾)が指定可能です。
fseek(fp, 10L, SEEK_SET); // 先頭から10バイト目へ
fseek(fp, -5L, SEEK_CUR); // 現在位置から5バイト戻る
fseek(fp, 0L, SEEK_END); // 末尾へ(ファイルサイズ取得に利用)
long size = ftell(fp); // 現在位置(=ファイルサイズ)
rewind(fp); // 先頭へリセット(fseek(fp, 0L, SEEK_SET) と等価)
テキスト vs バイナリファイル
テキストファイルは、改行コードの変換(例:\n → \r\n)やエンコーディング処理を伴い、人間が読める形式で保存されます。一方、バイナリファイルはメモリ上のビット列をそのまま保存・読み込みます。たとえば整数 10000 をバイナリで保存すれば4バイト、テキストで保存すれば5文字("10000")+改行で6バイト程度になります。
入力終了の安全な判定
feof() は「すでにEOFに達したか?」を確認する関数であり、読み込み前に使用するのは誤りです。正しい終了判定は各関数の戻り値に基づきます:
fgetc():正常時ASCII値、EOFまたはエラー時EOFfgets():成功時バッファアドレス、失敗時NULLfscanf():成功時読み込んだ項目数、失敗時0またはEOFfread():成功時読み込んだ要素数、部分読み込み時はそれより小さい値
終了後の原因特定に feof() と ferror() を併用します。
バッファリングとフラッシュ
Cの標準I/Oは内部バッファを使用します。書き込みはまずバッファに格納され、バッファが満杯または明示的なフラッシュ(fflush())が行われてからディスクへ転送されます。fclose() は自動的にバッファをフラッシュします。
大規模データの外部ソート
メモリに収まらない巨大ファイルをソートする場合、外部マージソートが有効です。手順は以下の通り:
- 分割段階:ファイルを小規模なチャンク(例:10件ずつ)に分割し、各チャンクをメモリ上でクイックソートして個別のファイルに保存
- マージ段階:ソート済みの小ファイルを2つずつマージし、結果を新たなファイルに書き出す(
file1 + file2 → merged12) - 反復マージ:得られたマージファイルと次の小ファイルを再度マージし、最終的に完全にソートされた単一ファイルを得る
この手法は、磁気ディスクのシーク時間と比較して極めて遅いランダムアクセスを最小限に抑え、効率的なO(n log n)ソートを実現します。