「ファイルから1行読み取る」といった作業は結構色々なところで使うものの,C で書くとなかなか面倒っちかったりします。
もちろん,標準ライブラリの string.h には,gets(3) や fgets(3) なんかがあるわけですけれど,前者はセキュリティ的に論外だし後者は読み取る文字数を指定しなくちゃいけません。100文字読み取るといったら,101文字は読み取れないわけで,結局1行がどれだけの長さか分からないと,当て勘でバッファを取るしかありません。また,入力する文字数を下手に信頼すると(HTTP レスポンスの Content-Length フィールドとか),オーバーフローの原因になったりもします。
というわけで,できるだけ安全にファイルから文字数の分からない行を読み取るルーチンを作ってみました。こういうのは大抵の方が自家製ライブラリとして持っているんでしょうけど,手前味噌ながらうちのルーチンをご紹介します。
早速紹介します。こんな感じ。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *get_string(FILE *fp, size_t buf_size)
{
char *buf;
char *new_buf;
char *ptr;
char *ret;
size_t read_size;
size_t len;
buf = NULL;
if ((buf = (char *)calloc(sizeof(char), buf_size)) == NULL) {
return NULL;
}
read_size = 0;
for (ptr = buf; ;ptr++, read_size++) {
if (read_size == buf_size) {
buf_size *= 2;
new_buf = NULL;
if ((new_buf = (char *)calloc(sizeof(char), buf_size)) == NULL) {
free(buf);
return NULL;
}
memcpy(new_buf, buf, read_size);
free(buf);
buf = new_buf;
ptr = buf + read_size;
}
fread(ptr, sizeof(char), 1, fp);
if (*ptr == '\n' || feof(fp)) {
*ptr = '\0';
break;
}
}
len = strlen(buf) + 1;
if ((ret = (char *)calloc(sizeof(char), len)) == NULL) {
free(buf);
return NULL;
}
strncpy(ret, buf, len);
free(buf);
return ret;
}
プロトタイプはこんな感じ。
char *get_string(FILE *, size_t);
引数にはファイルポインタ fp と,バッファの容量 size を指定します。バッファの容量というのは初期の容量で,バッファが足りなくなったら,もとの容量の2倍の領域を確保しなおしてコピーを続けます。最初に512バイトを指定したら,次は1024バイト,それでも足りなかったら2048バイト,といった具合。こういう方法は,割と常套句として使われているみたいです。
もっとも,確保しなおすなら小さめの値でもいいかというと,そういうわけでもなくて,頻繁に領域の確保と解放を繰り替えすと,ヒープをグチャグチャにしてしまいます。また,このルーチンは realloc(3) と使わずに,新しいバッファを全く別に作って,そこに既に読み込んだ分をコピーしなおしています。この点で,確保と解放を繰り替えすとパフォーマンス低下の原因になります。そんなわけで,バッファにはできるだけ大き目の値を指定しておくといいと思います。
get_string() は,1行分の文字列をバッファに一旦格納してから,後に文字数にピッタリの領域を作って格納しなおします。コピーしなおすので,パフォーマンス的には劣るんでしょうけれど,後々の使いやすさからこのまま実装してみました。返値はこのピッタリ領域のアドレスで,失敗したときは NULL を返します。また,行の終端にある改行('\n')とファイル終端(EOF)はヌル文字('\0')に変換されます。当たり前ですけれど,get_string() は,確保した領域を解放するところまでは面倒を見ないので,返ってきた領域は,責任を持って free() します。
で,この関数の実装例。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFSIZE 256
int main(int argc, char *argv[])
{
FILE *fp;
char *str;
int count;
if (argc != 2) {
fprintf(stderr, "arg error\n");
exit(-1);
}
if ((fp = fopen(argv[1], "r")) == NULL) {
fprintf(stderr, "error\n");
exit(-1);
}
for (count = 1; !feof(fp); count++) {
if ((str = get_string(fp, BUFSIZE)) == NULL) {
fprintf(stderr, "%s: string read error.\n", argv[0]);
exit(-1);
}
printf("%05d: %s\n", count, str);
free(str);
}
fclose(fp);
return 0;
}
コマンドラインでファイルを指定すると,行番号を付けて1行ずつ表示していきます。これくらいだったら,いちいち領域を確保する必要もないんですけどね。
参考になるかは分からないけれども,試しに FreeBSD の /usr/ports/INDEX-6 を読ませて time(1) を取ったところ,バッファのサイズによって,以下のような違いがありました。
size = 8
5.57s real 1.75s user 0.35s sys
size = 64
5.41s real 1.71s user 0.36s sys
size = 128
5.34s real 1.63s user 0.40s sys
size = 512
5.33s real 1.65s user 0.38s sys
size = 1024
5.22s real 1.71s user 0.31s sys
INDEX-6 は,現時点で14768行あります。バッファが大きいと,それなりにパフォーマンスもいいみたい。よく分かってませんけど。
realloc(3) を使わなかったのは,単に気分の話ですけれど,パフォーマンス的にもフラグメンテーションの心配から言っても,realloc(3) を使う方がいいっちゃいいのかもしれません。特にフラグメンテーションについては心配で,大量の行数を処理するとか,長い間実行し続けるプログラムなんかでは動作が不安定になるかもしれません(realloc(3) でもフラグメンテーションは起きるけど)。
まぁ,こういうのは1つくらい持ってると便利だよね,といった話。
追記:ファイル終端の判定が日和ってたので修正しました。
追記:ファイルポインタをクローズしときました。
追記:fread() する前にバッファを拡張することにしました。