C言語 fread関数の使い方入門 – サンプルコード付き

はい、承知いたしました。C言語のfread関数について、初心者向けに詳細な説明とサンプルコードを含む、約5000語の記事を作成します。


C言語 fread関数の使い方入門 – サンプルコード付き

ファイル入出力は、C言語に限らず、プログラミングにおいて非常に重要な操作です。データをファイルに保存したり、ファイルから読み込んだりすることで、プログラムの実行を超えて情報を永続化させたり、外部のデータを利用したりすることが可能になります。C言語の標準ライブラリ<stdio.h>は、ファイル操作のための豊富な関数を提供しており、その中でも特に、固定サイズのデータをまとめて読み込む際に強力な威力を発揮するのがfread関数です。

この記事では、C言語初心者の方でもfread関数をマスターできるように、その基本的な使い方から、引数や戻り値の詳細な解説、エラー処理の方法、さらには構造体やバイナリデータの読み込みといった応用例まで、サンプルコードを交えながら徹底的に解説していきます。約5000語に及ぶ詳細な解説を通じて、fread関数への理解を深め、様々なファイル入出力のタスクに対応できるようになることを目指します。

はじめに:ファイル入出力とfreadの役割

コンピュータの世界では、プログラムが扱うデータは通常、メインメモリ(RAM)上に一時的に存在します。プログラムが終了すると、そのデータは失われます。これでは、設定情報やユーザーが作成した文書、画像などのデータを永続的に保存しておくことができません。そこで登場するのがファイルシステムであり、データはストレージ(ハードディスク、SSDなど)上の「ファイル」として保存されます。

C言語を含む多くのプログラミング言語は、このファイルシステム上のデータとプログラムの間でデータをやり取りするための手段を提供しています。<stdio.h>ライブラリが提供する関数群は、この役割を担います。

ファイルからデータを読み込む基本的な方法としては、いくつかの選択肢があります。

  1. 文字単位で読み込む (fgetc): ファイルから1文字ずつ読み込む方法です。シンプルですが、大量のデータを扱う場合には非常に非効率です。
  2. 行単位で読み込む (fgets): ファイルから改行コードまで、あるいは指定された文字数までを読み込む方法です。テキストファイルを扱う際に便利ですが、バイナリデータ(画像や音声など、人間が読める文字として扱えないデータ)には向きません。
  3. 書式指定で読み込む (fscanf): ファイルから特定の書式(例えば %d で整数、%s で文字列など)に従ってデータを読み込み、変数に格納する方法です。テキストファイルの構造化されたデータを読み込むのに便利ですが、書式に厳密に従う必要があり、柔軟性に欠ける場合や、バイナリデータには不向きです。
  4. ブロック単位で読み込む (fread): ファイルから指定したサイズのデータを、指定した個数だけまとめて読み込む方法です。これがfreadです。文字や行、書式といった概念に縛られず、生のバイト列としてデータを扱えるため、バイナリデータの読み込みに非常に適しています。また、まとめて読み込むため、文字単位や書式指定で読み込むよりも一般的に高速です。

このように、fread関数は特に以下のような場合にその真価を発揮します。

  • 画像、音声、実行可能ファイルなど、バイナリ形式のファイルを読み込む場合。
  • 構造体や配列など、固定サイズのデータ構造をファイルに保存・読み込みする場合。
  • 大量のデータを効率的に読み込みたい場合。

以降のセクションでは、このfread関数の詳細な使い方を見ていきましょう。

fread関数の基本

fread関数のプロトタイプ宣言は、標準ライブラリ<stdio.h>の中で以下のように定義されています。

“`c

include

size_t fread(void ptr, size_t size, size_t nmemb, FILE stream);
“`

この1行の中に、fread関数のすべての情報が詰まっています。それぞれの要素が何を意味するのか、詳しく見ていきましょう。

引数の説明

fread関数は4つの引数を取ります。

  1. void *ptr:

    • : void * (汎用ポインタ)
    • 役割: 読み込んだデータを格納するメモリ領域を指すポインタです。void * 型になっているのは、どのような型のデータ(整数、浮動小数点数、構造体、単なるバイト列など)でも受け取れるようにするためです。実際に使用する際には、読み込みたいデータの型に応じたポインタ(例: int *, char *, struct MyData * など)を渡しますが、関数呼び出し時には void * に暗黙的に変換されます。
    • 注意点: このポインタが指すメモリ領域は、読み込むデータのサイズ (size * nmemb バイト) を格納できる十分な大きさが必要です。メモリの確保には malloc 関数などを使用するのが一般的ですが、固定サイズの場合は配列を宣言しても構いません。メモリが不足していると、バッファオーバーフローなどの未定義動作を引き起こす可能性があります。
  2. size_t size:

    • : size_t (符号なし整数型、通常は unsigned longunsigned int など、環境に依存)
    • 役割: 読み込み単位となる「1つの要素」のサイズをバイト単位で指定します。例えば、整数を1つ読むなら sizeof(int)、構造体を1つ読むなら sizeof(struct MyData) のように指定します。単なるバイト列として扱う場合は、要素サイズを1バイト (1) と指定することが多いです。
    • 注意点: この値は要素のサイズであり、読み込みたい合計バイト数ではありません。合計バイト数は size * nmemb で計算されます。
  3. size_t nmemb:

    • : size_t
    • 役割: size バイトの要素を何個読み込みたいかを指定します。例えば、整数を10個読むなら 10、構造体を100個読むなら 100 のように指定します。ファイル全体をある程度の塊(チャンク)に分けて読みたい場合は、バッファサイズを size に、バッファに格納できる要素数(バッファサイズ / 要素サイズ)を nmemb に指定してループで読み込むのが一般的です。
  4. FILE *stream:

    • : FILE * (ファイルポインタ)
    • 役割: データ読み込み元のファイルを指定します。このファイルポインタは、事前に fopen 関数を使って、読み込みモード(例: “rb” や “r”)で開かれている必要があります。fopen が失敗した場合や、ファイルが閉じられた後などに無効な FILE * を渡すと、未定義動作となります。

まとめると、fread(ptr, size, nmemb, stream) は、「stream から、1つあたり size バイトのデータを nmemb 個、合計 size * nmemb バイト読み込み、ptr が指すメモリ領域に格納してください」という意味になります。

戻り値の説明

fread関数の戻り値は size_t 型であり、実際に読み込みに成功した要素の数を示します。

  • 成功した場合: 要求した要素数 (nmemb) と同じ値が返されます。これは、size * nmemb バイトの読み込みが完全に成功したことを意味します。
  • 要求した要素数より少ない値が返された場合: これは、ファイルの終端(EOF)に達したか、あるいは読み込み中にエラーが発生したかのどちらかです。
    • ファイルの終端に達した場合: ファイルの最後までデータを読み込もうとした結果、要求した数より少ない要素しか存在しなかった場合に発生します。この場合、feof(stream) 関数は真を返します。
    • エラーが発生した場合: 読み込み中にディスクの読み取りエラーなどの問題が発生した場合です。この場合、ferror(stream) 関数は真を返します。

非常に重要な注意点: freadの戻り値は、読み込んだバイト数ではなく、読み込んだ要素の数です。例えば、fread(buffer, 4, 10, file) の呼び出しで7個の要素(合計 28 バイト)が読み込めた場合、戻り値は 7 となります。合計バイト数を取得したい場合は、戻り値 (items_read) に size を掛ける (items_read * size) 必要があります。

また、戻り値が 0 の場合は、以下のいずれかを示します。

  • size または nmemb のどちらか、あるいは両方が 0 だった場合(この場合は何も読み込まず、エラーでもEOFでもありません)。
  • ファイルの先頭から読み込みを開始しようとしたが、ファイルが空だった場合。
  • 読み込み中にエラーが発生したが、その前に何も要素を読み込めなかった場合 (ferror で確認)。
  • ファイルの終端に既に達していた場合 (feof で確認)。

したがって、fread の呼び出し後には、戻り値を確認するだけでなく、必要に応じて feof および ferror 関数を使って、読み込みがなぜ要求通りに行われなかったのか(EOFなのか、エラーなのか)を判断することが重要です。

fread関数を使うための準備:ファイルを開く・閉じる

fread関数を使うためには、まず読み込みたいファイルを「開く」必要があります。ファイルを開くには、fopen関数を使います。また、読み込みが終わったら、ファイルは「閉じる」必要があります。ファイルを開きっぱなしにしておくと、リソースのリークやデータの不整合などの問題を引き起こす可能性があります。ファイルを閉じるには、fclose関数を使います。

fopen関数

fopen関数のプロトタイプ宣言は以下の通りです。

“`c

include

FILE *fopen(const char * restrict filename, const char * restrict mode);
“`

  • filename: 開きたいファイルのパス名を指定する文字列です。
  • mode: ファイルの開き方を指定する文字列です。読み込みを行う場合は、以下のモードを指定します。
    • "r": テキストファイルを読み込みモードで開きます。ファイルが存在しない場合はエラーとなります。
    • "rb": バイナリファイルを読み込みモードで開きます。ファイルが存在しない場合はエラーとなります。

バイナリデータを扱う fread 関数を使う場合は、モードとして "rb" を指定するのが一般的です。テキストファイルを扱う場合でも、freadはバイト列として読み込むため "r" でも機能しますが、テキストモード特有の改行コード変換などの影響を受ける可能性があるため、特に理由がなければバイナリファイルを扱う際は "rb" を使う方が安全です。

fopen関数は、ファイルを開くことに成功した場合、そのファイルに対応する FILE 型のオブジェクトへのポインタ(ファイルポインタ)を返します。失敗した場合は、NULL を返します。fopenの戻り値は必ず確認し、NULL でない場合にのみファイル操作を行うようにする必要があります。

fclose関数

fclose関数のプロトタイプ宣言は以下の通りです。

“`c

include

int fclose(FILE *stream);
“`

  • stream: 閉じたいファイルに対応する FILE * ポインタを指定します。これは fopen 関数が返した値です。

fclose関数は、ファイルストリームをフラッシュし、システムリソースを解放してファイルを閉じます。成功した場合は 0 を返し、失敗した場合は EOF(通常は -1)を返します。通常、fcloseの戻り値を確認することは必須ではありませんが、重要なファイル操作の後など、必要に応じてエラーチェックを行うこともあります。

ファイル操作の基本的な流れ

fopen, fread, fclose を使ったファイル読み込みの基本的な流れは以下のようになります。

  1. 読み込むデータを格納するためのメモリ領域(バッファ)を準備する。
  2. fopen 関数を使ってファイルを読み込みモードで開く。
  3. fopenNULL を返していないか確認する。NULL の場合はエラー処理を行い、プログラムを終了するなどする。
  4. fopen が成功した場合、返された FILE * を使って fread 関数を呼び出し、データを読み込む。
  5. fread の戻り値を確認し、必要に応じて feofferror を使って、読み込みが成功したか、EOFに達したか、エラーが発生したかを判断する。
  6. 読み込んだデータを処理する。
  7. 必要であれば、データを読み込む操作(fread呼び出し、戻り値チェック、データ処理)を繰り返す。
  8. ファイルからの読み込みがすべて終わったら、fclose 関数を使ってファイルを閉じる。

この流れを意識して、具体的なサンプルコードを見ていきましょう。

fread関数の基本的な使い方:サンプルコード

最も基本的な例として、バイナリファイルから一定バイト数のデータを読み込み、それを画面に表示してみましょう。ここでは、ファイルから一度に10バイトずつ読み込む例を考えます。

まず、読み込み元のバイナリファイルが必要です。簡単なバイナリファイルを作成するために、fwrite関数を使ってファイルにいくつかのデータを書き込んでみましょう。

“`c
// create_binary_file.c

include

include

include

int main() {
FILE *file = fopen(“my_binary_data.bin”, “wb”); // バイナリ書き込みモードで開く
if (file == NULL) {
perror(“ファイルのオープンに失敗しました”);
return 1;
}

// 適当なバイナリデータ(例えばバイト列)を準備
unsigned char data_to_write[] = {
    0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
    0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
    0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E
};
size_t data_size = sizeof(data_to_write);

// データをファイルに書き込む (1バイトの要素をdata_size個)
size_t items_written = fwrite(data_to_write, 1, data_size, file);

if (items_written < data_size) {
    // エラーチェック (fwriteの戻り値は書き込んだ要素数)
    if (ferror(file)) {
        perror("ファイル書き込み中にエラーが発生しました");
    }
}

// ファイルを閉じる
if (fclose(file) == EOF) {
    perror("ファイルのクローズに失敗しました");
    return 1;
}

printf("バイナリファイル 'my_binary_data.bin' を作成しました。\n");

return 0;

}
“`

この create_binary_file.c をコンパイルして実行すると、my_binary_data.bin という名前のファイルが作成されます。このファイルには、30バイトのデータが含まれています。

次に、この my_binary_data.bin ファイルから fread を使ってデータを読み込んでみましょう。一度に10バイトずつ読み込み、読み込んだ内容を16進数で表示します。

“`c
// read_binary_file.c

include

include

int main() {
FILE *file = fopen(“my_binary_data.bin”, “rb”); // バイナリ読み込みモードで開く
if (file == NULL) {
perror(“ファイルのオープンに失敗しました”);
return 1;
}

// 読み込み用のバッファを準備
// 一度に10バイト読み込むので、10バイト分の領域が必要
unsigned char buffer[10];
size_t bytes_to_read = sizeof(buffer); // 10バイト

printf("ファイル 'my_binary_data.bin' からデータを読み込みます。\n");

// ファイルの終端またはエラーに達するまでループ
while (1) {
    // freadを呼び出し、1バイトの要素を bytes_to_read個読み込む
    size_t items_read = fread(buffer, 1, bytes_to_read, file);

    // 実際に読み込めたバイト数を計算 (要素数 * 要素サイズ)
    size_t actual_bytes_read = items_read * 1; // 要素サイズは1なので items_read と同じ

    if (actual_bytes_read > 0) {
        // データを読み込めた場合、内容を表示
        printf("読み込んだデータ (%zu バイト): ", actual_bytes_read);
        for (size_t i = 0; i < actual_bytes_read; i++) {
            printf("%02X ", buffer[i]); // 16進数で表示
        }
        printf("\n");
    }

    // 読み込みが完了したか、エラーが発生したかチェック
    if (items_read < bytes_to_read) {
        // 要求した数より少ない要素しか読み込めなかった場合
        if (feof(file)) {
            printf("ファイルの終端に達しました。\n");
        } else if (ferror(file)) {
            perror("ファイル読み込み中にエラーが発生しました");
        }
        break; // ループを終了
    }
    // 要求通り読み込めた場合はループ続行
}

// ファイルを閉じる
if (fclose(file) == EOF) {
    perror("ファイルのクローズに失敗しました");
    return 1;
}

printf("ファイルクローズ完了。\n");

return 0;

}
“`

この read_binary_file.c をコンパイルして実行すると、以下のような出力が得られるはずです(正確な出力は環境やファイルの内容によって微妙に異なる可能性がありますが、基本的なバイト列は同じです)。

ファイル 'my_binary_data.bin' からデータを読み込みます。
読み込んだデータ (10 バイト): 01 02 03 04 05 06 07 08 09 0A
読み込んだデータ (10 バイト): 0B 0C 0D 0E 0F 10 11 12 13 14
読み込んだデータ (10 バイト): 15 16 17 18 19 1A 1B 1C 1D 1E
ファイルの終端に達しました。
ファイルクローズ完了。

この例では、以下の点に注目してください。

  • ファイルを "rb" (read binary) モードで開いています。
  • 読み込み先のバッファ bufferunsigned char 型で宣言され、10バイトのサイズを持っています。unsigned char は1バイトの符号なし整数であり、バイナリデータをバイト単位で扱うのに適しています。
  • fread の呼び出しは fread(buffer, 1, bytes_to_read, file) となっています。これは、「file から、1つあたり 1バイト の要素を bytes_to_read (ここでは10) 個、つまり合計10バイト読み込み、buffer に格納してください」という意味です。
  • fread の戻り値 items_read は、実際に読み込めた要素の数です。要素サイズを1バイトとしているため、この場合は読み込めたバイト数と一致します。
  • while ループを使って、ファイル全体をチャンク(ここでは10バイト)に分けて読み込んでいます。
  • fread の戻り値が要求した数 (bytes_to_read) より少ない場合にループを終了しています。その際、feof(file)ferror(file) を使って、終了の原因がEOFなのかエラーなのかを判断しています。これは fread をループ内で使う際の典型的なパターンです。
  • 読み込んだ内容は、for ループを使って1バイトずつ取り出し、%02X という書式指定子(16進数で2桁表示、必要に応じてゼロ埋め)で表示しています。
  • 最後に fclose でファイルを閉じています。

この例を通じて、fread の基本的な呼び出し方、バッファの準備、ループによる全データ読み込み、そして戻り値とエラー/EOFの確認方法を理解できたかと思います。

fread関数の応用例

fread関数は、単なるバイト列だけでなく、C言語のデータ構造体や特定の型のデータを読み込むのにも非常に便利です。いくつかの応用例を見てみましょう。

応用例1:構造体をファイルから読み込む

プログラムで定義した構造体のデータをファイルに保存し、それを後で読み込むという操作はよく行われます。freadを使えば、構造体を「一つの要素」として捉え、まとめて読み込むことができます。

まず、書き込み用の構造体データを作成し、ファイルに保存するコードを考えます。

“`c
// write_structs.c

include

include

include

// サンプル構造体の定義
typedef struct {
int id;
char name[20];
double value;
} Record;

int main() {
FILE *file = fopen(“records.bin”, “wb”); // バイナリ書き込みモードで開く
if (file == NULL) {
perror(“ファイルのオープンに失敗しました”);
return 1;
}

// 書き込む構造体のデータを準備
Record records_to_write[3];

records_to_write[0].id = 101;
strncpy(records_to_write[0].name, "Alice", sizeof(records_to_write[0].name) - 1);
records_to_write[0].name[sizeof(records_to_write[0].name) - 1] = '\0'; // 終端文字を保証
records_to_write[0].value = 123.45;

records_to_write[1].id = 102;
strncpy(records_to_write[1].name, "Bob", sizeof(records_to_write[1].name) - 1);
records_to_write[1].name[sizeof(records_to_write[1].name) - 1] = '\0';
records_to_write[1].value = 67.89;

records_to_write[2].id = 103;
strncpy(records_to_write[2].name, "Charlie", sizeof(records_to_write[2].name) - 1);
records_to_write[2].name[sizeof(records_to_write[2].name) - 1] = '\0';
records_to_write[2].value = 987.65;

size_t num_records = sizeof(records_to_write) / sizeof(Record); // 構造体の個数

// 構造体の配列をファイルに書き込む
// 要素サイズを sizeof(Record) に、要素数を num_records に指定
size_t items_written = fwrite(records_to_write, sizeof(Record), num_records, file);

if (items_written < num_records) {
     if (ferror(file)) {
        perror("ファイル書き込み中にエラーが発生しました");
    } else {
        fprintf(stderr, "警告: 要求した構造体の数 (%zu) より少なく (%zu) しか書き込めませんでした。\n", num_records, items_written);
    }
}

// ファイルを閉じる
if (fclose(file) == EOF) {
    perror("ファイルのクローズに失敗しました");
    return 1;
}

printf("構造体データを含むバイナリファイル 'records.bin' を作成しました。\n");

return 0;

}
“`

このコードを実行すると、records.bin というファイルが作成されます。このファイルには、3つの Record 型構造体のデータが連続して保存されています。

次に、この records.bin ファイルから fread を使って構造体を読み込んでみましょう。

“`c
// read_structs.c

include

include

include

// 構造体の定義 (書き込み時と同じである必要があります)
typedef struct {
int id;
char name[20];
double value;
} Record;

int main() {
FILE *file = fopen(“records.bin”, “rb”); // バイナリ読み込みモードで開く
if (file == NULL) {
perror(“ファイルのオープンに失敗しました”);
return 1;
}

// 読み込み用の構造体変数を準備 (ここでは一度に1つずつ読み込む)
Record read_record;

printf("ファイル 'records.bin' から構造体を読み込みます。\n");

// ファイルの終端またはエラーに達するまでループ
int record_index = 0;
while (1) {
    // freadを呼び出し、sizeof(Record) バイトの要素を 1個 読み込む
    size_t items_read = fread(&read_record, sizeof(Record), 1, file);

    if (items_read == 1) {
        // 構造体を1つ読み込めた場合、内容を表示
        printf("Record %d:\n", record_index);
        printf("  ID: %d\n", read_record.id);
        printf("  Name: %s\n", read_record.name);
        printf("  Value: %f\n", read_record.value);
        record_index++;
    } else if (items_read == 0) {
        // 読み込みが0個だった場合
        if (feof(file)) {
            printf("ファイルの終端に達しました。\n");
        } else if (ferror(file)) {
            perror("ファイル読み込み中にエラーが発生しました");
        }
        break; // ループ終了
    } else {
        // 想定外の戻り値 (通常ありえないが念のため)
        fprintf(stderr, "エラー: freadから想定外の戻り値 %zu が返されました。\n", items_read);
        break; // ループ終了
    }
}

// ファイルを閉じる
if (fclose(file) == EOF) {
    perror("ファイルのクローズに失敗しました");
    return 1;
}

printf("ファイルクローズ完了。\n");

return 0;

}
“`

このコードを実行すると、records.bin ファイルから構造体が1つずつ読み込まれ、その内容が表示されます。

ファイル 'records.bin' から構造体を読み込みます。
Record 0:
ID: 101
Name: Alice
Value: 123.450000
Record 1:
ID: 102
Name: Bob
Value: 67.890000
Record 2:
ID: 103
Name: Charlie
Value: 987.650000
ファイルの終端に達しました。
ファイルクローズ完了。

この例では、freadsize 引数に sizeof(Record) を指定し、nmemb 引数に 1 を指定することで、「Record 構造体1つ分」のデータを読み込んでいます。ptr 引数には、読み込み先の Record 変数のアドレス &read_record を渡しています。

構造体のバイナリ読み書きに関する注意点:

  • パディング: コンパイラは構造体のメンバをメモリ上に配置する際に、アライメントのためにメンバ間に隙間(パディング)を挿入することがあります。sizeof(Record) はこのパディングを含んだサイズになります。ファイルをバイナリとしてそのまま読み書きする場合、このパディングもデータの一部として扱われます。異なるコンパイラやアーキテクチャで作成された構造体を持つファイルを読み書きしようとすると、パディングの入り方が異なり、データの解釈が狂う可能性があります。
  • エンディアン: 複数バイトで構成されるデータ型(int, double など)をメモリに配置する際のバイト順序(エンディアン)は、CPUアーキテクチャによって異なります(ビッグエンディアン、リトルエンディアン)。ファイルをバイナリとしてそのまま読み書きする場合、このエンディアンもそのままファイルに記録されます。異なるエンディアンを持つシステム間でバイナリファイルを交換すると、数値データが正しく解釈されない問題が発生します。
  • これらの問題を避けるためには、構造体を直接バイナリ読み書きするのではなく、各メンバを(エンディアンなどを考慮しながら)個別にバイナリ形式に変換してから書き込む、あるいはテキスト形式(CSV, JSONなど)で読み書きするなどの方法を検討する必要があります。しかし、同じシステム内でバイナリデータを高速に読み書きするという目的においては、構造体をそのまま扱うfread/fwriteは非常に便利です。

応用例2:ファイル全体をチャンクに分けて読み込む

最初の基本例でも行いましたが、ファイル全体を一度にメモリに読み込むにはメモリが不足する場合や、処理を小分けにしたい場合があります。このような場合は、ファイルを「チャンク」(塊)に分けて繰り返し読み込むのが一般的です。

“`c
// read_file_in_chunks.c

include

include

include // for strlen

// 読み込みバッファのサイズを定義 (例: 1KB = 1024バイト)

define CHUNK_SIZE 1024

int main() {
FILE *file = fopen(“large_text_file.txt”, “rb”); // バイナリ読み込みモード
if (file == NULL) {
perror(“ファイルのオープンに失敗しました”);
return 1;
}

// 読み込み用バッファを準備
unsigned char buffer[CHUNK_SIZE];

printf("ファイルを %d バイトずつのチャンクで読み込みます。\n", CHUNK_SIZE);

size_t total_bytes_read = 0; // 読み込んだ合計バイト数を記録

// ファイルの終端またはエラーに達するまでループ
while (1) {
    // freadを呼び出し、1バイトの要素を CHUNK_SIZE 個読み込む
    size_t items_read = fread(buffer, 1, CHUNK_SIZE, file);

    // 実際に読み込めたバイト数を計算
    size_t actual_bytes_read = items_read * 1; // 要素サイズは1

    if (actual_bytes_read > 0) {
        // 読み込んだデータを処理 (この例では単にバイト数をカウント)
        // printf("読み込んだチャンク (%zu バイト): ...\n", actual_bytes_read);
        // 実際のアプリケーションでは、bufferの内容を処理する
        total_bytes_read += actual_bytes_read;
    }

    // 読み込みが完了したか、エラーが発生したかチェック
    if (items_read < CHUNK_SIZE) {
        // 要求した数より少ない要素しか読み込めなかった場合
        if (feof(file)) {
            printf("ファイルの終端に達しました。\n");
        } else if (ferror(file)) {
            perror("ファイル読み込み中にエラーが発生しました");
        }
        break; // ループ終了
    }
    // 要求通り読み込めた場合はループ続行
}

printf("ファイルの読み込みが完了しました。合計 %zu バイト読み込みました。\n", total_bytes_read);

// ファイルを閉じる
if (fclose(file) == EOF) {
    perror("ファイルのクローズに失敗しました");
    return 1;
}

printf("ファイルクローズ完了。\n");

return 0;

}
“`

このコードを実行するには、事前に large_text_file.txt という名前のファイルを作成しておく必要があります。例えば、適当な長いテキストをコピー&ペーストしてファイルに保存しておくと良いでしょう。

この例では、CHUNK_SIZE マクロで読み込むチャンクサイズを定義しています。fread はこのサイズの塊を繰り返し読み込みます。while(1) ループ内で fread を呼び出し、戻り値が CHUNK_SIZE より小さい場合にループを終了するというパターンは、ファイル全体をチャンクで読み込む際の標準的な手法です。

応用例3:特定のデータ型の読み込み

ファイルに保存された整数や浮動小数点数などの特定のデータ型を読み込む場合も、fread が使えます。ただし、構造体の例と同様にエンディアンの問題には注意が必要です。

“`c
// read_specific_types.c

include

include

include // 固定幅整数型を使う場合

int main() {
FILE *file = fopen(“numeric_data.bin”, “rb”); // バイナリ読み込みモード
if (file == NULL) {
perror(“ファイルのオープンに失敗しました”);
return 1;
}

// 読み込む変数を準備
int my_int;
double my_double;
uint32_t my_uint32; // 32ビット符号なし整数 (固定幅整数型)

printf("ファイル 'numeric_data.bin' から数値を読み込みます。\n");

// ファイルから int を1つ読み込む
size_t items_read_int = fread(&my_int, sizeof(int), 1, file);
if (items_read_int == 1) {
    printf("読み込んだ int: %d\n", my_int);
} else {
    fprintf(stderr, "int の読み込みに失敗しました (EOF or Error)。\n");
    // エラーまたはEOFの確認
    if (feof(file)) printf("(原因: EOF)\n");
    if (ferror(file)) perror("(原因)");
}

// ファイルから double を1つ読み込む
size_t items_read_double = fread(&my_double, sizeof(double), 1, file);
if (items_read_double == 1) {
    printf("読み込んだ double: %f\n", my_double);
} else {
    fprintf(stderr, "double の読み込みに失敗しました (EOF or Error)。\n");
    if (feof(file)) printf("(原因: EOF)\n");
    if (ferror(file)) perror("(原因)");
}

// ファイルから uint32_t を1つ読み込む
size_t items_read_uint32 = fread(&my_uint32, sizeof(uint32_t), 1, file);
if (items_read_uint32 == 1) {
    printf("読み込んだ uint32_t: %u (0x%X)\n", my_uint32, my_uint32);
} else {
    fprintf(stderr, "uint32_t の読み込みに失敗しました (EOF or Error)。\n");
    if (feof(file)) printf("(原因: EOF)\n");
    if (ferror(file)) perror("(原因)");
}

// ファイルを閉じる
if (fclose(file) == EOF) {
    perror("ファイルのクローズに失敗しました");
    return 1;
}

printf("ファイルクローズ完了。\n");

return 0;

}
“`

このコードを実行するには、事前に numeric_data.bin というファイルに、sizeof(int) バイト、sizeof(double) バイト、sizeof(uint32_t) バイトのデータがこの順でバイナリ形式で書き込まれている必要があります。例えば、以下のコードでファイルを作成できます。

“`c
// create_numeric_data.c

include

include

include

int main() {
FILE *file = fopen(“numeric_data.bin”, “wb”); // バイナリ書き込みモード
if (file == NULL) {
perror(“ファイルのオープンに失敗しました”);
return 1;
}

int val_int = 12345;
double val_double = 3.14159;
uint32_t val_uint32 = 0xAABBCCDD;

fwrite(&val_int, sizeof(int), 1, file);
fwrite(&val_double, sizeof(double), 1, file);
fwrite(&val_uint32, sizeof(uint32_t), 1, file);

if (fclose(file) == EOF) {
    perror("ファイルのクローズに失敗しました");
    return 1;
}

printf("数値データを含むバイナリファイル 'numeric_data.bin' を作成しました。\n");

return 0;

}
“`

create_numeric_data.c を実行後、read_specific_types.c を実行すると、ファイルから読み込んだ数値が表示されるはずです。ここでも、freadsize 引数に sizeof() 演算子を使って読み込みたい型のサイズを指定し、nmemb1 を指定することで、その型のデータを1つ読み込んでいます。

エンディアンの問題再説: 上記の例で作成・読み込みするファイルは、同じシステム上で実行することを前提としています。異なるエンディアンのシステム間でこのファイルをやり取りすると、123450xAABBCCDD といった値が正しく読み込めない可能性があります。クロスプラットフォームなバイナリファイル形式を扱う場合は、エンディアンを標準化したり、バイトオーダー変換を行うライブラリを利用したりする必要があります。

freadのエラー処理とEOFの確認

前述の通り、fread の戻り値が nmemb より小さい場合は、読み込みが要求通りに行われなかったことを意味します。その原因がファイルの終端(EOF)なのか、それともエラーなのかを正確に判断するためには、feof 関数と ferror 関数を使用します。

  • int feof(FILE *stream);:
    • ファイルストリーム stream がファイルの終端に達している場合にゼロ以外の値を返します。
    • まだファイルの終端に達していない場合や、エラーが発生している場合はゼロを返します。
  • int ferror(FILE *stream);:
    • ファイルストリーム stream で読み書きエラーが発生している場合にゼロ以外の値を返します。
    • エラーが発生していない場合や、EOFに達している場合はゼロを返します。

これらの関数は、fread(または fwrite や他のファイルI/O関数)を呼び出したに呼び出す必要があります。fread の戻り値が nmemb より小さい場合、以下のフローで確認するのが一般的です。

“`c
size_t items_read = fread(buffer, size, nmemb, file);

if (items_read < nmemb) {
// 要求した数より少なくしか読み込めなかった
if (ferror(file)) {
// エラーフラグが立っている -> 読み込み中にエラーが発生した
perror(“ファイル読み込みエラー”);
// 適切なエラー処理(例えばプログラムの終了やエラーログ記録)
} else if (feof(file)) {
// エラーフラグは立っていないが、EOFフラグが立っている -> ファイルの終端に達した
printf(“ファイルの終端に達しました。\n”);
// 読み込めた items_read 個のデータを処理する
} else {
// ferrorもfeofも真でないが、戻り値がnmembより小さいという稀なケース
// 標準では起こりにくいが、念のため考慮する
fprintf(stderr, “不明な fread 戻り値 (< nmemb) です。\n”);
}
} else {
// items_read == nmemb -> 要求通り読み込みに成功した
// 読み込んだ nmemb 個のデータを処理する
}
“`

ループ処理でファイルをチャンク読み込みする場合、fread の戻り値が nmemb より小さくなったらループを終了するのが自然な流れです。そのループの直後に、feofferror をチェックして、ループを終了した原因がEOFなのかエラーなのかを特定します。

“`c
while (1) {
size_t items_read = fread(buffer, size, nmemb, file);

// 読み込めた items_read 個のデータを処理する
// (実際のバイト数は items_read * size)
process_data(buffer, items_read * size);

if (items_read < nmemb) {
    // 要求した数より少なくしか読み込めなかった場合、ループを終了
    break;
}

}

// ループ終了後、原因をチェック
if (ferror(file)) {
perror(“ファイル読み込みエラー”);
} else if (feof(file)) {
printf(“ファイルの終端に達しました。\n”);
}
“`

このパターンが、ファイル全体をfreadで読み込む際の最も堅牢なエラー・EOF処理方法と言えます。

freadと他の入力関数の比較

freadがどのような場面で有用かをより明確にするために、他の標準入力関数と比較してみましょう。

  • fgetc: 1文字(1バイト)ずつ読み込みます。非常に低速ですが、任意の場所から1バイトだけ読み込みたい場合に便利です。fread(buffer, 1, 1, file) と等価ですが、通常はfgetcの方がシンプルに使えます。
  • fgets: 行単位で読み込みます。テキストファイルを扱う際には非常に便利ですが、改行コードを基準とするためバイナリデータには向きません。また、指定したバッファサイズや改行コードの有無によって読み込みバイト数が変動します。freadのように固定サイズ・個数で読み込む用途とは異なります。
  • fscanf: 書式指定に従ってデータを読み込みます。テキストファイルから特定の形式のデータを抽出するのに便利ですが、書式に厳密に従う必要があり、柔軟性に欠ける場合があります。書式変換のオーバーヘッドがあり、バイナリデータには全く向きません。
関数 読み込み単位 主な用途 バイナリデータ 効率 戻り値の型・意味
fgetc 1文字 (1バイト) 1文字ずつ、テキスト ○ (バイトとして) 低速 int (文字コード or EOF)
fgets 1行 テキスト、行単位 × 中速 char * (バッファ or NULL)
fscanf 書式指定 テキスト、書式ありデータ × 中速 int (読み込んだ項目数 or EOF)
fread ブロック (size * nmemb バイト) バイナリ、固定サイズ/個数、効率的な読み込み 高速 size_t (読み込んだ要素数)

このように、freadは「固定サイズのデータブロックを効率的に、生のバイト列として読み込む」という、他の関数にはない明確な役割を持っています。

freadとバッファリング、パフォーマンス

C言語の標準ファイル入出力関数は、通常、バッファリングを利用しています。これは、ファイルへのアクセス回数を減らし、パフォーマンスを向上させるための仕組みです。

ファイルから1バイトずつ読み込む場合でも、システムは実際にはディスクからより大きな塊(例えば数KBや数十KB)のデータを一度に読み込み、それをメモリ上の「バッファ」に格納します。そして、fgetc などで要求された1バイトは、このバッファから提供されます。バッファが空になったら、再び大きな塊を読み込みます。ファイルへの書き込みも同様で、データは一旦バッファに蓄えられ、バッファがいっぱいになったり、ファイルが閉じられたり、明示的にフラッシュされたりするタイミングでまとめてディスクに書き込まれます。

fread関数は、このバッファリングの仕組みとうまく連携します。freadは指定された size * nmemb バイトのデータを要求しますが、これはシステムバッファから効率的に取得されます。freadが要求するサイズがシステムバッファのサイズよりも小さい場合、ディスクへのアクセスはfreadの呼び出しごとには発生せず、バッファが空になったときにまとめて行われます。freadが要求するサイズが大きい場合でも、fread自体が内部でバッファリングを考慮して効率的な読み込みを行います。

freadを使ってファイル全体をチャンクに分けて読み込む場合、チャンクサイズ(size * nmemb の合計バイト数)を適切に選ぶことがパフォーマンスに影響します。あまりに小さいチャンクサイズにすると、freadの呼び出し回数が増え、関数呼び出しのオーバーヘッドが無視できなくなる可能性があります。逆に、あまりに大きいチャンクサイズにすると、大量のメモリが必要になり、キャッシュミスが増えたり、システムの他の処理に影響を与えたりする可能性があります。一般的には、数KBから数十KB程度のチャンクサイズが適切とされることが多いですが、最適なサイズはシステムの特性やアプリケーションの要件によって異なります。

freadの一般的な落とし穴と注意点

freadを使う上で、初心者が陥りやすい落とし穴や、注意すべき点があります。

  1. 戻り値をバイト数と勘違いする: 最もよくある間違いです。freadの戻り値はあくまで「要素数」です。読み込んだ合計バイト数は 戻り値 * size で計算する必要があります。
  2. 戻り値のチェックを怠る: freadが要求通りの数の要素を読み込めなかった場合(戻り値 < nmemb)、ファイル処理が正しく行われません。特にループで全ファイルを読み込む場合、戻り値のチェックとループ終了条件の設定は必須です。
  3. エラー/EOFのチェックを怠る: 読み込みが途中で終了した原因がEOFなのかエラーなのかを区別せずに処理を進めると、意図しない動作になる可能性があります。特に重要なファイルを扱う場合は、ferrorfeofによる詳細なチェックが必要です。
  4. バッファサイズの不足: freadが読み込んだデータを格納するptrが指すメモリ領域は、最低でも size * nmemb バイトの大きさが必要です。これより小さいバッファを渡すと、バッファオーバーフローが発生し、プログラムのクラッシュやセキュリティ上の脆弱性につながる可能性があります。常に sizeof() 演算子を使って、バッファのサイズと読み込むデータのサイズが合っているかを確認しましょう。
  5. バイナリモード ("rb") で開かない: テキストファイルを "r" モードで開いた場合、OSによっては改行コードの自動変換が行われることがあります(例えば、LFからCR+LFへの変換など)。freadは生のバイト列としてデータを読み込むため、このような変換が行われると、期待したバイト数やデータ内容と異なる結果になる可能性があります。バイナリファイルを扱う場合や、改行コード変換を避けたい場合は、必ず "rb" モードで開いてください。
  6. 構造体のパディングやエンディアンを無視する: 異なる環境間で構造体のバイナリデータをやり取りする場合、パディングやエンディアンの違いによってデータの解釈が狂う問題が発生します。これはfread関数自体の問題ではなく、バイナリデータのプラットフォーム依存性によるものです。クロスプラットフォーム対応が必要な場合は、より移植性の高いデータ形式(テキスト形式、あるいは標準化されたバイナリ形式)を選ぶか、エンディアン変換などの処理をコードに加える必要があります。
  7. ファイルを開かずにfreadを呼び出す: freadの第4引数streamは、fopenが成功して返した有効なFILE *ポインタである必要があります。fopenが失敗した場合(戻り値がNULL)や、既にfcloseで閉じられたファイルポインタを渡すと、未定義動作となります。必ずfopenの戻り値をチェックし、ファイルポインタが有効であることを確認してからfreadを呼び出してください。
  8. ファイルクローズを忘れる: fopenで開いたファイルは、使い終わったら必ずfcloseで閉じなければなりません。これを怠ると、プログラム終了までファイルがロックされたままになったり、バッファに残ったデータがディスクに書き込まれずに失われたり、システムリソースが解放されなかったりといった問題が発生します。エラー発生時など、通常の処理フロー以外の場合でもfcloseが呼ばれるように、エラーハンドリングの中にfcloseを含めるなどの工夫が必要です。

これらの注意点を意識することで、fread関数を安全かつ効果的に使用することができます。

freadfwrite

freadfwriteは、それぞれバイナリデータの読み込みと書き込みを行う関数であり、しばしば対で利用されます。

“`c

include

size_t fwrite(const void ptr, size_t size, size_t nmemb, FILE stream);
“`

fwritefreadとよく似た引数を取ります。

  • const void *ptr: 書き込むデータが格納されているメモリ領域を指すポインタ。
  • size_t size: 書き込む「1つの要素」のサイズ(バイト単位)。
  • size_t nmemb: size バイトの要素を何個書き込むか。
  • FILE *stream: 書き込み先のファイルポインタ(”wb”, “ab” などの書き込みモードで開かれている必要あり)。

fwriteの戻り値は、実際に書き込みに成功した要素の数です。freadと同様に、要求した数 (nmemb) より少ない値が返された場合は、書き込みエラーが発生したことを意味します (ferror で確認)。

freadfwriteを組み合わせることで、あるファイルから読み込んだバイナリデータを別のファイルにそのままコピーしたり、メモリ上の構造体データをファイルに保存したり、ファイルから読み込んだデータをメモリ上で加工して再びファイルに書き戻したりといった処理が可能になります。

例えば、ファイルを別のファイルにコピーする簡単な例は以下のようになります。

“`c
// copy_file.c

include

include

define CHUNK_SIZE 4096 // 4KB

int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, “使用法: %s <入力ファイル> <出力ファイル>\n”, argv[0]);
return 1;
}

FILE *input_file = fopen(argv[1], "rb"); // 入力ファイルをバイナリ読み込みモードで開く
if (input_file == NULL) {
    perror("入力ファイルのオープンに失敗しました");
    return 1;
}

FILE *output_file = fopen(argv[2], "wb"); // 出力ファイルをバイナリ書き込みモードで開く
if (output_file == NULL) {
    perror("出力ファイルのオープンに失敗しました");
    fclose(input_file); // 入力ファイルは開けたので閉じる
    return 1;
}

unsigned char buffer[CHUNK_SIZE];
size_t items_read;
size_t items_written;

// 入力ファイルから読み込み、出力ファイルに書き込むループ
while ((items_read = fread(buffer, 1, CHUNK_SIZE, input_file)) > 0) {
    // 読み込んだデータをそのまま出力ファイルに書き込む
    items_written = fwrite(buffer, 1, items_read, output_file);

    if (items_written < items_read) {
        // 書き込みエラー
        perror("ファイル書き込みエラー");
        fclose(input_file);
        fclose(output_file);
        return 1;
    }
}

// ループ終了後、読み込みエラーをチェック
if (ferror(input_file)) {
    perror("入力ファイル読み込みエラー");
    fclose(input_file);
    fclose(output_file);
    return 1;
}

// ファイルを閉じる
if (fclose(input_file) == EOF) {
    perror("入力ファイルのクローズに失敗しました");
    // 続行して出力ファイルも閉じようとする
}
if (fclose(output_file) == EOF) {
    perror("出力ファイルのクローズに失敗しました");
    // 続行
}

printf("ファイル '%s' を '%s' にコピーしました。\n", argv[1], argv[2]);

return 0;

}
“`

この例では、fread でチャンクを読み込み、その読み込めたバイト数 (items_read) をそのまま fwritenmemb 引数に指定して書き込んでいます。freadfwritesize 引数をどちらも 1 とすることで、バイト単位のコピーを実現しています。

まとめ

この記事では、C言語の fread 関数について、その基本的な使い方から詳細な仕様、応用例、エラー処理、そして関連関数との比較まで、幅広く解説しました。

fread関数は、size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); というプロトタイプを持ち、ファイルストリームから指定したサイズの要素を、指定した個数だけまとめて読み込むために使用されます。特にバイナリデータの効率的な読み込みや、固定サイズのデータ構造体(構造体、配列など)のファイル入出力において非常に強力なツールです。

freadを正しく安全に使うためには、以下の点を理解し、実践することが重要です。

  1. 引数の意味: ptr (格納先バッファ)、size (要素サイズ)、nmemb (要素数)、stream (ファイルポインタ) を正確に理解する。
  2. 戻り値の解釈: 戻り値は読み込んだ要素数であり、バイト数ではないことを常に意識する。
  3. ファイル開閉: fopenで適切なモード(通常 "rb")で開き、fcloseで必ず閉じる。
  4. バッファサイズの確保: size * nmemb バイト以上の十分なメモリをptrが指すように確保する。
  5. エラー・EOF処理: freadの戻り値が要求数より少ない場合は、feofferrorを使って原因(EOFまたはエラー)を正確に判断し、適切に処理する。特にループによる全ファイル読み込みでは、このチェックが必須。
  6. バイナリデータの注意点: 構造体をそのまま読み書きする場合のパディングやエンディアンの問題に留意する。
  7. 他の関数との使い分け: テキストの文字単位、行単位、書式付き入力にはそれぞれ適した関数(fgetc, fgets, fscanf)があり、freadはブロック/バイナリ読み込みに特化していることを理解する。

fread関数は、C言語で高度なファイル操作を行う上で欠かせない要素です。この記事で学んだ知識とサンプルコードを参考に、ぜひ実際に様々なファイル処理に挑戦してみてください。データの永続化や外部データソースとの連携といった、より複雑なプログラムを作成するための強力な武器となるはずです。

ファイル操作は、プログラムが現実世界のデータとやり取りするための重要な窓口です。freadを使いこなすことで、その窓口を効率的かつ正確に扱う能力が大きく向上します。

これで、C言語のfread関数の使い方入門に関する詳細な記事は終わりです。約5000語という目標に対し、freadの基本から応用、注意点まで、網羅的かつ詳細に解説することを試みました。読者の皆様の学習の一助となれば幸いです。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール