🌟

[課題振り返り] ft_printf, get_next_line編

2025/01/21に公開

はじめに

先日42Tokyoのコモンコアを突破しました。前回に引き続き、今回はft_printfとget_next_lineを振り返っていきたいと思います。

ft_printf

課題概要

C言語の標準ライブラリ関数であるprintf関数を再実装する課題です。

学んだこと

学んだことや山場となったポイントをまとめていきます。

可変長引数

printfの再実装はやはり可変長引数の扱いがハードルになってきます。
以下に基本となる使い方がわかるコードを載せます。
(コード内でprintf関数を使用していますがこれは出力結果を見るためのものでここでは可変長引数とは関係ありません)

#include <stdarg.h> // va_を使用するためのヘッダ
#include <stdio.h>

int sum_of_integers(int count, ...)
{
	va_list args;
	va_start(args, count); // count(固定引数)を指定することでその次の引数を先頭としてva_listの初期化

	int sum = 0;
	for (int i = 0; i < count; i++)
	{
		int number = va_arg(args, int); //型を指定して引数の取り出し
		printf("%d\n", number);
		sum += number;
	}

	va_end(args); // va_listの終了
	return sum;
}

int main(void)
{
	int sum = sum_of_integers(3, 1, 2, 3);
	printf("%d\n", sum);
	return 0;
}
(実行結果)
$ cc test.c  
$ ./a.out  
1
2
3
6

sum_of_integers関数が、可変長引数("..."の部分)を受け取る関数です。
va_list型が可変長引数を扱うための型です。
また、可変長引数は必ずその前に固定引数を必要とします。(コードではcount)
va_start(va_listを初期化する)の引数にこの固定引数を渡すことでva_list型がどの引数が先頭なのかを識別できます。(固定引数が複数ある場合でも、可変長引数の直前の固定引数を渡す必要があります)
以後、va_argで可変長引数の前から順に取り出していくことができます。(その取り出す引数の型を指定する必要があります)
取り出しきったらva_endを実行する必要があります。
va_endはva_listが使用しているリソースを開放してくれます。

get_next_line

課題概要

複数行文字が書かれたファイルディスクリプタを受け取り、実行されるたびに行ごとに返す関数をC言語で書く。

学んだこと

read関数

そもそもfdから読み取るということ自体はじめはよく分からなかったのですが、readについて個人的に引っかかったポイントを紹介します。
それはreadが同じfdに対して複数回実行された場合、前回の続きから読んでくれるということです。
例えば以下のような感じです。

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

void my_read(int fd, char *line, int size)
{
	read(fd, line, size);
}

int main(void)
{
	int fd = open("test.txt", O_RDONLY);
	char line[6];

	my_read(fd, line, 5);
	printf("%s\n", line);
	my_read(fd, line, 5);
	printf("%s\n", line);

	return (0);
}
(実行結果) 
$ echo 'Hello,World!' > test.txt 
$ cc test.c
$ ./a.out 
Hello
,Worl

このように関数を分けるとreadは続きから読んでくれます。
人によっては当たり前と感じるかもしれませんが、筆者はなぜかまた最初から読むと勘違いしていました。
このように続きから読んでくれるのはファイルディスクリプタが読み書き位置(オフセット)を管理しているファイルポインタを内部で保持しているからです。

static変数

static変数は最初の一回のみ初期化され、スコープは宣言した範囲内だが変数の寿命はプログラムの終了まで続くというものです。
なので、その関数を抜けたとしても値が保持されます。

#include <stdio.h>

void say_hello(void)
{
	static int i = 0; // static変数
	printf("This is %d, Hello, World!\n", i);
	i++;
}

int main(void)
{
	say_hello();
	say_hello();
	say_hello();

	return (0);
}
(実行結果)
$ cc test.c
$ ./a.out 
This is 0, Hello, World!
This is 1, Hello, World!
This is 2, Hello, World!

このように関数を抜けても値が保持されていることが確認できます。
(staticでなかった場合すべて0と出力されます。)

一度にfdからreadするBUFFER_SIZEを実行時に指定できるようにする

これがget_next_lineが難しくなっている原因だと思います。
例えば1文字ずつreadして良いなら'\n'まで読み取ってそれを返すだけで済みますが、一度にBUFFER_SIZE分読み取るようにしなきゃいけないので、その読み取ったBUFFER_SIZE内に'\n'が1文字も入っていない可能性もあれば2文字入っている可能性もあります。
そこで先ほどのstatic変数を使用する必要があります。
static変数に'\n'の後余分にreadした分は次の行の内容なので保持しておき、次回よばれた時にその文字列を含めて処理をします。

システムコールなどが失敗した時のメモリリーク

mallocやreadなどのシステムコールは失敗することがあるのですが、その際に何も考えずにプログラムを終了するとメモリリークが起こってしまいます。(メモリリークについて後日詳しい記事を書こうと思います 書きました->https://zenn.dev/kodyi/articles/2a4b7f58058838)
なので必ず失敗した時の挙動も細かく考える必要があります。

Discussion