Skip to content

C言語でのファイル作成と読み書き:よくある問題と解決策

問題の概要

C言語でファイルを作成し、書き込んだ内容をすぐに読み取ろうとした際、期待した内容が表示されない問題に遭遇することがあります。以下のコード例では、「Hello, World!」と書き込んだはずなのに、読み取り時に何も表示されません。

c
#include <stdio.h>

#define SIZE 256
int main() {
  FILE *fptr = fopen("mem.txt","w+"); // ファイル作成/上書き(読み書き両用)
  char buffer[SIZE + 1]; // null文字分も確保
  buffer[SIZE] = '\0';

  if(!fptr) { // ファイル作成失敗時にNULL
    printf("Failed creation");
    return -1;
  }

  printf("Creation successful\n");
  fprintf(fptr, "Hello, World!"); // ファイルに書き込み

  fread(buffer, sizeof(buffer), 1, fptr); // すぐに読み取り
  printf("%s", buffer);

  fclose(fptr);
  return 0;
}

期待される出力:

Creation successful
Hello, World!

実際の出力:

Creation successful
(何も表示されない)

根本原因:ファイル位置の管理

この問題の根本原因は、ファイルストリームの現在位置(カーソル位置)の管理にあります。

ファイル位置とは

ファイルを操作する際、C言語は内部で「現在位置」を追跡しています。書き込みや読み取りを行うたびに、この位置は自動的に進みます。

書き込み後に読み取りを行う場合、標準Cではファイル位置関数を介在させる必要があります(C標準 7.23.5.3p7)。

解決策1: ファイル位置のリセット

最も簡単な解決策は、rewind()関数を使用してファイルの先頭に戻る方法です。

c
fprintf(fptr, "Hello, World!"); // 書き込み
rewind(fptr); // ファイル位置を先頭にリセット
fread(buffer, sizeof(buffer), 1, fptr); // 読み取り
printf("%s", buffer);

あるいは、fseek()を使用して明示的に位置を指定することもできます。

c
fprintf(fptr, "Hello, World!");
fseek(fptr, 0, SEEK_SET); // オフセット0に移動(先頭)
fread(buffer, sizeof(buffer), 1, fptr);
printf("%s", buffer);

解決策2: fread()の適切な使用方法

元のコードには別の問題も潜んでいます。fread()の使用方法とバッファ処理に注意が必要です。

fread()の正しい引数順序

c
// 問題のある使い方
fread(buffer, sizeof(buffer), 1, fptr);

// 推奨する使い方
size_t length = fread(buffer, 1, sizeof(buffer), fptr);

最初の方法では「1個のsizeof(buffer)サイズの要素」を読み取ろうとしますが、ファイルサイズが小さい場合、部分的な読み取りとなり未定義動作を引き起こします。

文字列として安全に扱う方法

c
// 方法1: 明示的にnull終端する
size_t length = fread(buffer, 1, sizeof(buffer) - 1, fptr);
buffer[length] = '\0';
printf("%s", buffer);

// 方法2: 精度指定子を使用する
size_t length = fread(buffer, 1, sizeof(buffer), fptr);
printf("%.*s", (int)length, buffer);

解決策3: 代替関数の使用

fgets()を使用する方法

テキストファイルの読み取りにはfgets()がより適しています。

c
if (fgets(buffer, sizeof buffer, fptr)) {
  printf("%s", buffer);
}

fgets()は自動的にnull文字で終端するため、文字列処理が簡単です。

fwrite()で出力する方法

バイナリデータや正確な出力が必要な場合:

c
size_t length = fread(buffer, 1, sizeof(buffer), fptr);
fwrite(buffer, 1, length, stdout); // 標準出力に直接書き出し

高度な解決策: 低レベルファイル操作

POSIXシステムでは、より低レベルなファイル操作も利用できます。

c
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    const int fd = open("mem.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    
    const char text[] = "Hello, World!";
    pwrite(fd, text, sizeof(text) - 1, 0); // 書き込み(オフセット指定)
    
    char buffer[sizeof(text)];
    ssize_t readSize = pread(fd, buffer, sizeof(buffer) - 1, 0); // 読み取り(オフセット指定)
    buffer[readSize] = 0;
    
    puts(buffer);
    close(fd);
}

注意点

低レベルファイル操作はバッファリングされないため、パフォーマンス特性が異なります。また、POSIXシステム以外では利用できない場合があります。

バッファリングに関する考慮事項

Cの標準I/O関数はバッファリングを使用します。これはパフォーマンス向上のためですが、書き込み後すぐに読み取る場合には問題を引き起こす可能性があります。

バッファリングの制御

fflush(fptr)を使用して明示的にバッファをフラッシュすることもできますが、ファイル位置のリセットも同時に行う必要があります。

まとめ

C言語でファイルの書き込み後すぐに読み取る場合の主要な解決策:

  1. ファイル位置のリセット: rewind()またはfseek()を使用
  2. 安全な読み取り: fread()の正しい使用方法と適切なバッファ処理
  3. 代替関数の使用: fgets()fwrite()の検討
  4. 低レベルAPI: プラットフォーム固有の機能が必要な場合

これらのテクニックを理解することで、C言語のファイル操作でよくある落とし穴を回避し、信頼性の高いコードを書くことができます。