fgetcで書ければ、freadで書ける

初心者では、fgetcで一文字づつ処理するコードは記述できるのに、バッファを使うfreadで書くのに悩む人もいます。

単純にfgetcで一文字づつ処理するコードは、以下のような形になるでしょう

for (;;) {
  int ch = fgetc(file);
  if (ch == EOF) break;
  ...
}

これをバッファとfreadを使う形式に変換すると以下のようになるだけです(buf, i, countはかぶらない前提で):

char buf[BUF_SIZE];
for (;;) {
  int i;
  int count = fread(buf, sizeof(char), BUF_SIZE, file);
  for (i = 0; i < count; i++) {
    char ch = buf[i];
    ...
  }
  if (count < BUF_SIZE) break;
}

(追記: fgetc,freadともに、終了判定条件は、feof(file) のほうがよいかもしれない。)

できるかぎり単純化したもので考えれば、freadを使うということとfgetcを使うのとに大差はないのがわかるのではないでしょうか。

同様な、文字単位読み込み⇔バッファ単位読み込みは、Javaのio⇔nioでもおきますが、これも同様にそんなに概念差があるものではないです。

ふと思ったのは、fgetcを使うとき最初にwhile ( (ch = fgetc(file) ) != EOF) {...}と覚えさせてしまうことが、freadの壁をより大きくしてるかもしれないな、と。

例: readlines

標準の関数(stdlib/stdio)だけを使って、ファイル全体を読み込んで、文字列の配列(配列の最後はNULL)を返すchar ** readlines(FILE*)関数の実装を考えてみましょう。

この関数は、擬似コードで書くと以下のようになります:

char ** readlines(FILE * file)
{
  char ** lines;
  int lineindex = 0;
  int index = 0;
  
  linesバッファの確保;
  for (;;) {
    int ch = fgetc(file);
    if (ch == EOF) break;
    if (ch == CRの直後のLF) continue;
    
    linesバッファの拡張;
    lines[lineindex]バッファの確保;
    lines[lineindex]バッファの拡張;
    
    if (ch == CR || ch == LF) {
      lineindex++;
      index = 0;
    } else {
      lines[lineindex][index] = ch;
      index++;
    }
  }
  return lines;
}

実際のコードでは定数定義、バッファの確保や進展、収縮、最後に0を入れる等をすることになるので長くなりますが、そのあたりを省略してあります。


実際のCコードでは、以下のようになります

char ** readlines(FILE * file)
{
  enum {LINE_SIZE = 80};
  enum {LINES_SIZE = 25};
  enum {CR = '\r'};
  enum {LF = '\n'};
  
  int lineindex = 0;
  int lines_size = LINES_SIZE;
  char ** lines = NULL;
  int index = 0;
  int line_size = 0;
  int is_prev_cr = 0;
  
  assert(file != NULL);
  lines = malloc(lines_size * sizeof(char *));
  if (lines == NULL) goto error;
  lines[0] = NULL;
  for (;;) {
    int ch = fgetc(file);
    if (ch == EOF) break;
    if (is_prev_cr) {
      is_prev_cr = 0;
      if (ch == LF) continue;
    }
    
    if (lineindex + 1 == lines_size) {
      char ** expanded;
      lines_size *= 2;
      expanded = realloc(lines, lines_size * sizeof(char *));
      if (expanded == NULL) goto error;
      lines = expanded;
    }
    if (lines[lineindex] == NULL) {
      line_size = LINE_SIZE;
      lines[lineindex] = malloc(line_size * sizeof(char));
      if (lines[lineindex] == NULL) goto error;
      lines[lineindex + 1] = NULL;
      lines[lineindex][0] = '\0';
    }
    if (index + 1 == line_size) {
      char * expanded;
      line_size *= 2;
      expanded = realloc(lines[lineindex], line_size * sizeof(char));
      if (expanded == NULL) goto error;
      lines[lineindex] = expanded;
    }
    
    if (ch == CR) is_prev_cr = 1;
    if (ch == CR || ch == LF) {
      realloc(lines[lineindex], (index + 1) * sizeof(char));
      lineindex++;
      index = 0;
    } else {
      lines[lineindex][index] = (char) ch;
      index++;
      lines[lineindex][index] = '\0';
    }
  }
  realloc(lines, (lineindex + 2) * sizeof(char *));
  return lines;
error:
  freelines(lines);
  return NULL;
}

これをfreadを使うようにすると、機械的に以下のように変更できます:

char ** readlines(FILE * file)
{
  enum {LINES_SIZE = 32};
  enum {LINE_SIZE = 128};
  enum {BUF_SIZE = 1024};
  enum {CR = '\r'};
  enum {LF = '\n'};
  
  int lineindex = 0;
  int lines_size = LINES_SIZE;
  char ** lines = NULL;
  int index = 0;
  int line_size = 0;
  int is_prev_cr = 0;
  char buf[BUF_SIZE];
  
  assert(file != NULL);
  lines = malloc(lines_size * sizeof(char *));
  if (lines == NULL) goto error;
  lines[0] = NULL;
  for (;;) {
    int i = 0;
    int count = fread(buf, sizeof(char), BUF_SIZE, file);
    for (i = 0; i < count; i++) {
      char ch = buf[i];
      if (is_prev_cr) {
        is_prev_cr = 0;
        if (ch == LF) continue;
      }
      
      if (lineindex + 1 == lines_size) {
        char ** expanded;
        lines_size *= 2;
        expanded = realloc(lines, lines_size * sizeof(char *));
        if (expanded == NULL) goto error;
        lines = expanded;
      }
      if (lines[lineindex] == NULL) {
        line_size = LINE_SIZE;
        lines[lineindex] = malloc(line_size * sizeof(char));
        if (lines[lineindex] == NULL) goto error;
        lines[lineindex + 1] = NULL;
        lines[lineindex][0] = '\0';
      }
      if (index + 1 == line_size) {
        char * expanded;
        line_size *= 2;
        expanded = realloc(lines[lineindex], line_size * sizeof(char));
        if (expanded == NULL) goto error;
        lines[lineindex] = expanded;
      }
      
      if (ch == CR) is_prev_cr = 1;
      if (ch == CR || ch == LF) {
        realloc(lines[lineindex], (index + 1) * sizeof(char));
        lineindex++;
        index = 0;
      } else {
        lines[lineindex][index] = (char) ch;
        index++;
        lines[lineindex][index] = '\0';
      }
    }
    if (feof(file)) break;
  }
  realloc(lines, (lineindex + 2) * sizeof(char *));
  return lines;
error:
  freelines(lines);
  return NULL;
}

ここから、たとえばデータコピーで最適化したり、直接読み込ませたりすることを考えることができたりしそうです。


ちなみにfreelinesは以下のコードです

void freelines(char ** lines)
{
  char ** cursor;
  if (lines == NULL) return;
  for (cursor = lines; *cursor; ++cursor) free(*cursor);
  free(lines);
}