読者です 読者をやめる 読者になる 読者になる

テキストファイルの全部を読んで行単位にする

プログラミング言語 C でテキスト処理をしようと思うと何かと面倒なことは多い。 ただ行ごとに読むというだけのことにでもひどく手間をかけさせられてしまう。 それをなんとか工夫して実装しようとしている記事を読んだ。

もう 20 年ほど前のことになるが、私はチャット CGI を C で書いたことがあり、その中で似たようなことをやったというのを思い出した。 そのときの技法 (というほど大したものではないが) を紹介したいと思う。

考え方

結局ファイル全部を読むのだから一度に読んでしまえば良いではないかというのが基本的な発想だ。 fread 関数を使ってテキスト全部を読めば改行で区切られたひとつの文字列が出来上がる。

f:id:SaitoAtsushi:20160129033040p:plain

改行をヌル文字に置換えた上でそれぞれの行の先頭を指すポインタの配列を作れば、見掛け上は行ごとの文字列になっているかのように見える。

f:id:SaitoAtsushi:20160129033241p:plain

ちなみに、以下の実装では改行が 0d 0a の2バイトの場合、 0a だけの場合のどちらにも対応している。

実装

// whole.h
char** whole(const char* const filename);
void freewhole(char** m);
// whole.c
#include <stdio.h>
#include <stdlib.h>

static long int filesize(FILE* fp) {
  fseek(fp, 0, SEEK_END);
  long int size = ftell(fp);
  fseek(fp, 0, SEEK_SET);
  return size;
}

static int countline(char* block) {
  int line = 0;

  for(int i=0, flag=0; block[i]; i++)
    switch(block[i]) {
    case '\r': line++; flag=1; break;
    case '\n': if(flag) flag = 0; else line++; break;
    default: flag = 0;
    }

  return line+2;
}

static char* nextline(char* str) {
  char* n;
  for(n=str; *n!='\n' && *n!='\r' && *n!='\0'; n++);
  switch(*n) {
  case '\0': return NULL;
  case '\r': *n++='\0'; if(*n=='\n') n++; if(*n=='\0') return NULL; break;
  case '\n': *n++='\0'; if(*n=='\0') return NULL; break;
  }
  return n;
}

static char* readall(const char* const filename) {
  FILE* fp = fopen(filename, "rb");
  if(fp==NULL) {perror(NULL); exit(EXIT_FAILURE); }
  int size = filesize(fp);
  char* block = malloc(size+1);
  if(block==NULL) {perror(NULL); exit(EXIT_FAILURE); }
  int read_size = fread(block, 1, size, fp);
  fclose(fp);
  block[read_size]='\0';
  return block;
}

static char** split(char* block) {
  int linelength=countline(block);
  char** lines = malloc(linelength*sizeof(char*));
  if(lines==NULL) {perror(NULL); exit(EXIT_FAILURE); }
  char** p;
  for(p=lines; block!=NULL; block=nextline(block)) *p++=block;
  *p=NULL;
  return lines;
}

char** whole(const char* const filename) {
  char* block = readall(filename);
  return split(block);
}

void freewhole(char** m) {
  free(m[0]);
  free(m);
}

使用例

// test.c
#include <stdio.h>
#include "whole.h"

int main(void) {
  char** lines = whole("test.txt");

  for(int i=0; lines[i]; i++) printf("%d %s\n", i, lines[i]);

  freewhole(lines);
  
  return 0;
}

利点と欠点

まず利点としては、

  • 考え方が単純でわかりやすい
  • コードが短い
  • コストの大きい処理であるメモリの割付け (malloc) の回数が抑えられている (ので高速である)
  • コストの大きい処理であるメモリの再割付け (realloc) がない (ので高速である)
  • コストの大きい処理である配列のコピーがない (ので高速である)

といった点が挙げられる。

欠点としては、

  • 巨大な連続したメモリ領域が必要

ということがある。 全体のメモリ使用量としては少なくても、行ごとにメモリを確保するのではなくファイルサイズと同じだけのメモリの塊が必要なので、状況によってはメモリ確保に失敗しやすくなる可能性はある。

Document ID: bc963f8df9035ce7f2619ed40ec345b6