This project has been created as part of the 42 curriculum by ymiyakos.
C標準ライブラリの printf() を再実装し、静的ライブラリ (libftprintf.a) としてビルドする。
glibc の printf() の再実装を通して可変長引数の扱い方、どのようにしてprintfが実装されているのかを学ぶ。
| 指定子 | 出力 |
|---|---|
%c |
1文字 |
%s |
文字列 |
%p |
ポインタ(16進数) |
%d / %i |
符号付き10進数 |
%u |
符号なし10進数 |
%x / %X |
符号なし16進数(小文字 / 大文字) |
%% |
% そのもの |
format文字列を1文字ずつ走査し、%を見つけたら%の次の文字を見て対応する引数を出力する- 変換指定子ごとの分岐は
put_args()に集約し、走査と出力を分けることでft_printf()本体を単純に保っている
- 変換指定子ごとの分岐は
ft_printf()の戻り値は出力したバイト数write()が失敗したら -1 を返し、以降の出力を打ち切る- 出力したバイト数が INT_MAX を超える場合も -1 を返す(戻り値が
intであり、加算でオーバーフローさせないため、加算前にtotal > INT_MAX - resultで判定している) %の直後が\0の場合は -1 を返す%sの NULL は(null)、%pの NULL は(nil)を出力する(glibc のprintf()に合わせた挙動)
%u%x%X%pは「基数が違うだけの符号なし整数の出力」なので、基数を表す文字列("0123456789abcdef"など)を渡すft_putunbr()1つにまとめた- 基数の文字列を渡す形にすることで、桁数の計算と文字の対応づけを1か所で済ませられ、大文字・小文字の違いも渡す文字列を変えるだけで表現できる
- 引数を
unsigned longにしているのは、unsigned intとuintptr_t(ポインタ値)のどちらも同じ関数で受けるため %d%iはft_putnbr()で符号を出力した後に絶対値をft_putunbr()へ渡す- 引数を
long longにしているのは、INT_MINの符号を反転してもintのようにオーバーフローしないため
- 引数を
- ポインタは
intやunsigned intとは幅が異なる(64bit環境では8バイト)ため、va_arg(*ap, void *)で正しい型として取り出す必要がある - そのため
put_args()側で引数を取り出さず、ft_putptr()にva_listへのポインタごと渡して、関数内でva_argを行っているva_listを値渡しすると、呼び出された側で進めた位置が呼び出し元に反映されず、次の引数を正しく読めなくなる場合があるため、ポインタで渡している
- Linux の
write()は1回の呼び出しで最大0x7ffff000バイト(= INT_MAX - 4095)までしか書き込まない- INT_MAX をページサイズ(4096バイト)の境界に切り下げた値であるため、 INT_MAX ではなく INT_MAX - 4095 になる
- これを超える長さの文字列は、
ft_putstr()で INT_MAX - 4095 バイトずつに分けてwrite()を繰り返し、 glibc のprintf()と同様に最後まで出力する write()の戻り値が要求した長さより短い場合(部分書き込み)に備え、実際に書き込めたバイト数だけ進めて残りを再度書き込んでいる
make # libftprintf.a をビルド
make clean # オブジェクトファイルを削除
make fclean # オブジェクトファイルと libftprintf.a を削除
make re # 最初から再ビルド#include "ft_printf.h"
int main(void)
{
ft_printf("Hello, %s! %d\n", "world", 42);
return (0);
}cc -Wall -Wextra -Werror -I include main.c libftprintf.atest/ ディレクトリに、本物の printf と出力・戻り値を比較するテスターがある。
make -C test run # 通常テスト
make -C test big # + write の上限テスト (約2GiBのメモリを使う)- テストケース相談
- テスト関数の設計相談
- テスト関数の実装(全て確認済み)
- 雛形の作成(本文は自分で記述)