C言語におけるファイル入出力の実践と応用

プログラムが終了すると、メモリ上に保持されていたデータは失われます。たとえば、構造体を用いて実装した連絡先管理アプリケーションでは、実行中に追加・削除した情報がプロセス終了とともに消滅し、次回起動時には再入力が必要になります。これを回避するには、データを永続化——すなわち、外部ストレージ(例:ディスク上のファイルやデータベース)に保存する必要があります。

ファイルの基本概念

プログラム設計において、「ファイル」は大きく二種類に分類されます。

  • プログラムファイル:ソースコード(.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またはエラー時 EOF
  • fgets():成功時バッファアドレス、失敗時 NULL
  • fscanf():成功時読み込んだ項目数、失敗時 0 または EOF
  • fread():成功時読み込んだ要素数、部分読み込み時はそれより小さい値

終了後の原因特定に feof()ferror() を併用します。

バッファリングとフラッシュ

Cの標準I/Oは内部バッファを使用します。書き込みはまずバッファに格納され、バッファが満杯または明示的なフラッシュ(fflush())が行われてからディスクへ転送されます。fclose() は自動的にバッファをフラッシュします。

大規模データの外部ソート

メモリに収まらない巨大ファイルをソートする場合、外部マージソートが有効です。手順は以下の通り:

  1. 分割段階:ファイルを小規模なチャンク(例:10件ずつ)に分割し、各チャンクをメモリ上でクイックソートして個別のファイルに保存
  2. マージ段階:ソート済みの小ファイルを2つずつマージし、結果を新たなファイルに書き出す(file1 + file2 → merged12
  3. 反復マージ:得られたマージファイルと次の小ファイルを再度マージし、最終的に完全にソートされた単一ファイルを得る

この手法は、磁気ディスクのシーク時間と比較して極めて遅いランダムアクセスを最小限に抑え、効率的なO(n log n)ソートを実現します。

タグ: c-language file-io buffering external-sorting binary-files

6月1日 23:01 投稿