📝

1時間で書くGet Next Line(GNL)

2024/04/21に公開

by 課題のハードルを下げようの会

課題

static変数を使用してバッファリングしながら、ファイルから1行読み込んで文字列として返す関数を作る。

方針

scrapboxでよく紹介されている方法は複雑な分岐が発生して辛いです。
色々な実装方法が考えられますが、Examにも出るので、ササッと1時間ぐらいで書けるような実装方法を考える必要があります。
自作getcから作ると、簡単に実装できるのでオススメです。

他の実装でもgetcを使って実装してるよ

getlineをgetcを使用して実装してる例
https://opensource.apple.com/source/cvs/cvs-29/cvs/lib/getline.c.auto.html
https://github.com/lattera/freebsd/blob/master/contrib/file/getline.c

まずgetc()を実装しよう

僕のオススメはまずはgetc(3)を実装することです。
K&R(カニハン)の209ページにgetchar(単一バッファ版)のとてもきれいな実装があるのでそれを参考にします。(getcharというのは標準入力から1文字読み取って、1文字返す関数です。)

B.W.カーニハン/D.M.リッチー: プログラミング言語C 第2版, 共立出版(1989),  p.209-210

getchar
/* getchar: 単一バッファ版 */
int getchar(void)
{
  static char buf[BUFSIZ];
  static char *pufp;
  static int n = 0;

  if(n == 0) { /* バッファはカラ */
    n = read(0, buf, sizeof buf);
    bufp = buf;
  }
  return (--n >= 0) ? (unsignd char) *bufp++ : EOF;
}

もしstatic変数に慣れていないと、プログラムの意味がよく意味が分からないかもしれませんが、最初はNormも無視して、一旦写経して動かしてみて下さい。(写経とコピペは違うよ。面倒くさくても意味を考えながら書き写すとコピペでは気づかなかったことが色々見えてきます。)

関数に引数からファイル記述子を受け取って、read(0,・・・のところをfdに変更すれば、ファイル記述子版getcの完成です。
わーい

ボーナスをする場合は、3つのstatic変数を構造体にして1つにまとめればよいです。
readに失敗した場合、返り値が負の値になるのでエラー処理が必要です。詳しくはman 2 readを読んで下さい。

ここから先は若干ネタバレ

putc()を作ろう

バッファリングしながら1文字づつ読み込む関数ft_getcができたので、今度は1文字つづメモリに書き込むft_putc関数を作ろう。毎回1文字づつ書き込むといっても、その度にmallocして、コピーしいては遅くなってしまうし、エラー処理が面倒くさかったりするので、可能であれば呼び出す回数を減らしたいです。
getcでバッファリングして、read回数を減らしたように、putcでもある程度まとめてmallocしたいです。ただし、1行の長さを事前に知ることはできないので、足りなくなったらある程度大きめに容量を再確保する形にします。

typedef struct s_string {
    char *str; // 文字列
    size_t len; // 文字列の長さ
    size_t capa; // 確保した領域の大きさ
} t_string;

int ft_putc(t_string *str, char c)
{
    if(str->len + 1 >= str->capa) {
        // 領域が足りなくなったら新たに確保して、コピー
    }
    str->str[str->len] = c; // 一文字詰め込む
    str->len++;
    return 0;
}

capaの増やし方は色々あると思いますが、capaを2倍にして再確保することが多いかな。最終的な文字列の長さがNとした時、O(log(N))回の再確保で済むので。

後は文字列を改行まで読み取るだけ

バッファリングしつつ、1文字づつ読み取る関数できたので、後は改行またはEOFが来るまで文字列に1文字づつ詰めて、返してあげればget_next_lineの完成です。

char *get_next_line(int fd) {
    // 今回の場合はt_string  *strではなく、 t_string strとしておけば、
    // 構造体をmalloc & freeする必要がないので実装が楽になります。
    t_string str; 
    char c;

    // 初期化
    str.str = NULL;
    str.len = 0;
    str.capa = 0;

    while(1) { // 無限ループ
        c = ft_getc(fd); // 1文字読み込む

        if(c == EOF) {
            break; // ファイルの最後ならループから抜ける
        }
        ft_putc(&str, c); // 1文字詰める
        if(c == '\n') {
            break; // 改行だったらループから抜ける
        }
    }
    if(str.len > 0) {
        ft_putc(&str, '\0'); // 最後にNULL文字を詰める
    }
    return str.str;
}

大まかな流れはこんな感じになると思いますが、ft_getcやft_putcはエラーを返す可能性があるので、エラー処理をちゃんとしましょう(エラー処理の時にstr.strのfreeも忘れずに)。

Discussion