[C言語]printfを一時間で書く [42Tokyo][動画あり]
42Tokyoではいろいろな勉強をしていて
今までも浮動小数点数のエンコードの記事を書いたりしました
42の課題で「printf」を作るというものがあって、それは本家printfと同じ挙動を再現するというものなんですが、
今回は、変換指定子 %d, %x, %sでwidth/precisionのみ対応, flagはなしという簡易バージョンを1時間で書いてみようということにチャレンジしたので紹介したいと思います
動画中は、コーディングするのに必死であまり説明できていないので、時間ごとにどういうことをやってるかについてこのnoteでは説明して行きたいと思います
※ 見出しごとにその場所に飛べるようにしておきましたので良ければ見ていただければ幸いです
Testを書く 1:53
本家のprintfを再現するという課題なので、本家の挙動がそのまま正解になります
自分の関数がうまく動いているか一目瞭然です
タイムアタック的な中でテストを書くかどうかは悩ましいところではありますが
テストケースも多くなってきたときにいちいち目で確認してられないので、少しタイムロスは痛いですが、テストは最初に書いていくと良い気がします
テストする方法ですが、
printfは標準出力に吐く関数なので、ビルド時にdefineで自分の関数を呼ぶか本家の関数を呼ぶかを分けてコンパイルして、実行結果をファイルに出力してdiffを取るのが手っ取り早いのでそうします
#ifdef FT_PRINTF
#define F(...) \
res = ft_printf(__VA_ARGS__); \
printf("%d\n", res);
#else
#define F(...) \
res = printf(__VA_ARGS__); \
printf("%d\n", res);
#endif
こんな感じですね
preprocessorで可変長引数を扱うには __VA_ARGS__が使えます
関数でやろうと思うと、ちょっと面倒(vprintfを実装してap_listを渡せるようにするとか)なので、#defineでやるのがいいと思います
ft_putstr 部分を作る: 6:05
最初に、一番簡単なformatをそのまま出力する部分を作っていきます
ft_printf("hoge");
みたいなものです
これは
int.ft_putstr(char *str)
{
int.res;
if (!str)
return (0);
res = 0;
while (*str)
{
res += ft_putchar(*str);
str++;
}
return (res);
}
のような関数を実装します
whileループでstrのポインタを1文字ずつインクリメントして
int ft_putchar(char c)
{
return write(1, &c, sizeof(char));
}
このような関数を呼び出して、char型の文字を標準出力にwriteしているだけです
これがprintfのベースとなる出力関数になるので、こいつを使いまわすようにしておきます
format内の%以降のw/pをparseする 13:08
%sを作るにあたって
formatの中に %が出現したときに、width/precisionをparse出来るようにする部分を先に作ってしまいます(read_argsと呼んでます)
%[width].[precision][conversion]
という表記に対して、
typedef struct s_args
{
int c;
int width;
int has_width;
int precision;
int has_precision;
} t_args;
こういった構造体に詰め込む処理を書いていきます
width/precisionを読むにあたって、%の後ろに来る文字が数字であれば、width、「.」であれば、widthがなくて、precisionのみ
というような区別があるので、%の後ろに来た文字が数字かどうかを判定する関数を書いていきます
charが数字かどうかを判定する関数を作成する 15:02
というわけで、
与えられた char型の変数cが数字かどうかを判定します
文字リテラルの '0'はasciiコードで表すと48、'9'は57になるので、48以上57以下であれば、数字であると言えます
こういうときは文字リテラルで比較するようにしておくと便利です
return (c >= '0' && c <= '9');
こんな感じですね
ついでに、ここでは、後々、atoiを書くときのためにisspaceも実装しています
isspaceの対象となる文字は
man isspace
に書いてあります
atoiを書く 17:05
というわけで、%直後のwidthを読み込んでいくわけですが
文字列をintegerに変換するatoiを作ります
atoiは
man atoi
を読むと、
The atoi() function converts the initial portion of the string pointed to by str to
int representation.
It is equivalent to:
(int)strtol(str, (char **)NULL, 10);
と書いてあります
なので、strtolというchar*を受け取って、longを返すfunctionを書いてintにcastするというのが正しい挙動のようです
なので、underflow/overflowする場合のみ
strtolの仕様に則って、LONG_MIN/LONG_MAXを返すようにしています
基本的なロジックは
1文字ずつ読んで、10倍して足していくことで生成できます
overflowするかunderflowするかの判定部分に関しては
max = 0x7fffffffffffffff;
という値を入れておいて、10倍して足したときにmaxを超えるか?
という判定になりますが、10倍した値がoverflowすると元も子もないので、移項しているのがポイントです
実は、ここで、signをかけるのを忘れていたのですが、width/precisionを読み込む時、今回の仕様では「-」のフラグを想定していないのでセーフでした
数字の桁数を取得する関数 ft_get_digitsを書きます 25:35
これで、width/precisionの数字が読み込める様になったので、読み込んだ分だけstrのポインタをすすめるために
数字が何桁だったかを取得する関数を書いておきます
実は、atoiするときに同じような処理をしているので、atoiしながらポインタを進めることもできるっちゃできるんですが
多少のオーバーヘッドよりもわかりやすさを優先しています
intの最小値のときだけ例外処理として11を即値で返すようにしておいて
-の時に-の分の桁数を足して、正の整数に変換して、桁数を計算します
これで、width/precision/.がそれぞれ読み込めて構造体が完成しました
put_sを作っていきます 31:28
構造体の情報を使って、指定子に実際置換する文字列を出力していく処理を書いていきます
ft_printf("%s", "hoge");
のような処理です
ここで、可変長引数を読み込むためにstdarg.hをincludeします
va_start(ap, format)
予約されたレジスタの領域に可変長のデータをコピーしておいて
va_arg(ap, char *)
などとして、レジスタからロードしつつオフセットを動かすみたいなことをやってくれるやつですね
%sの時のwidthとprecisionの扱いにだけ注意が必要です
%sの時には、precisionは最大文字数になります、出力したい文字列が"hoge"でprecisionが3が指定されていれば、"hog"だけが出力されます
ちなみに、与えられた文字列NULLの場合は、"(null)"という文字列が入力されているのと同じになるという面白い仕様があります
後は、widthは実際に出力される文字数 + 空白がwidthか、文字列の長さのどちらか大きい方に揃うようにします
ft_strlen(文字列の長さを計算する), ft_putstrl(文字列を指定した文字数だけ出力する)などを一緒に実装しておくと楽ちんです
put_sのデバッグ 38:24
ここまで快調に飛ばしていましたが、いくつかバグを抱えていました
% を読み込んだときに、インクリメントしていない
has_precisionがfalseのときもprecisionを0にしていたので、文字が出力されない
conversionを読み込むときに != '%'としないといけないところを == にしていた・・
など、ここで結構大幅に時間をロスしてしまいました。
put_dを作っていく 47:53
put_sと同じように出力するだけですね
sと違うところは、precisionが出力する数字の桁数よりも大きいときは、0でpaddingされるという仕様になっていて、sとは全く違います
また、注意しないといけないのは出力したい数字が-のときは-の分の桁数を考慮しないということです
つまり
-1234という数字が渡されてprecisionが6のときは
-001234となります
そして、実際にwidthを計算する際は、上記は7文字としてカウントされます
そのへんに注意して実装していきます
ここでは、新たに、ft_puti(d, padding);という関数を作って
paddingで与えられた0の数を考慮しつつ数字をアウトプットするようにしています。
%dの罠 58:41
で、さらに、もう一つ%dを実装する上で注意する必要があるところがあります
それは、precisionが定義されていて、かつprecisionが0で数字が0のようなケースです
%.d
%.0d
みたいなケースです
この時、数字として与えられるのは0なので、桁数を取得すると1になるわけですが、precisionは0なので、他の%dと同じで考えると0が出力されるはずですが、このケースは、''として扱われます
なので以下のような出力結果になるようにする必要があります
printf("[%d]\n", 0); // -> [0]
printf("[%.d]\n", 0); // -> []
printf("[%3.d]\n", 0); // -> [ ]
printf("[%3.0d]\n", 0);// -> [ ]
put_x を作る 1:03:52
put_xはput_dができていれば、難しいところは特にありません
16進数で、桁数を計算するようにするところ (ft_get_digits_x)
16進数を、出力していくところ (ft_putx)
をそれぞれ計算します
-値は補数として計算する必要があるのですが、これは単にunsigned intにキャストしてあげればよいだけですね
完成! 1:14:10
完成です!!!
return valueのテスト 1:15:20
と思っていたんですが、返り値のテストをしていませんでした。
返り値のテストは、ft_printfの返り値も一緒に標準出力に吐いておいてそれも比較するようにします
ここでprintfのbufferingをオフにしておかないと出力順序がぐちゃぐちゃになっちゃうことがあるのだけ注意です
(ここはあまり詳しくないので、ささっと・・)
setvbufという関数を呼べば良いようです。(また勉強したらいつか補足します)
最後に
今回この動画を取るまでに、だいたい3回ぐらい練習したんですが
段々と書くスピードが早くなりましたし、つまりどころもわかってきて、スムーズに書ける様になってきました
なかなか普段同じプログラムを何度も繰り返す書くということはないので非常にレアな体験で楽しかったです
是非皆さんもprintfタイムアタック挑戦してみてください!
いろいろと勉強になると思います
今回書いたコードはこちら
この記事が気に入ったらサポートをしてみませんか?