42Tokyo、入学後2つ目の課題printfの再実装で出会った可変長引数マクロ、わかったようなわからないような。モヤモヤ解消のために、Geminiの助けを借りてmanを丁寧に読んでみます。
C言語でprintfのような「引数の数が決まっていない関数」を作るために必要なのがstdarg.hです。複雑に見えますが、本質は「スタック上の引数を順番に指し示すポインターの操作」です。
3つのステップと1つの後片付け
可変長引数の操作は、常に以下の4つのマクロで行われます。
va_start:開始準備
void va_start(va_list ap, last);
可変長変数はどこから始めるかを決め、先頭の引数にポインターをセットします。第2引数 last は「可変長引数が始まる直前の変数名」。内部的には last のメモリアドレスを取得し、そのすぐ隣から可変長リストが始まると判断します。va_startを実行すると、last変数のすぐ隣にポインタが立ちます。 va_argを呼ぶたびに、そのポインタが指定した型(intなら4〜8バイト分など)だけズレながら値を取ってくるイメージです。
va_arg: 値の取り出し
type va_arg(va_list ap, type);
型を指定して進むこのマクロは「現在の値」を返すと同時に、apを次の引数へ進めます。C言語は実行時に型情報を持たないため、type(intやchar *など)を明示する必要があります。罠: char や float を渡すときは注意が必要です。char は int として、float は double として取り出す必要があります。char を可変長引数に渡すと、C言語のルール(既定の実引数拡張 default argument promotion)により、自動的に 整数拡張(integer promotion) が適用され、int サイズに拡張されてスタックに積まれます。そのため、取り出すときも int として取り出さないと、メモリの読み込み位置がズレてしまうのです。
va_copy:状態のコピー
void va_copy(va_list dest, va_list src);
va_startを再度呼ぶのではなく、現在の読み取り位置を別の変数に保存します。
va_end:終了処理
void va_end(va_list ap);
使い終わったポインターを無効化します。
実装例:型指定子に応じた出力関数
manにはfooという名前の実装例がありますが、これは実質的にprinfの実装サンプルコードです。少しだけ分かりやすく整理しました。
#include <stdarg.h>
#include <stdio.h>
void my_printf(char *fmt, ...) {
va_list ap, ap_backup;
va_start(ap, fmt); // fmtの次からスキャン開始
va_copy(ap_backup, ap); // 念のためバックアップ(後で再利用可能)
while (*fmt) {
switch (*fmt++) {
case 's': { // 文字列
char *s = va_arg(ap, char *);
printf("string: %s\n", s);
break;
}
case 'd': { // 整数
int d = va_arg(ap, int);
printf("int: %d\n", d);
break;
}
case 'c': { // 文字(intとして取り出す!)
int c = va_arg(ap, int);
printf("char: %c\n", c);
break;
}
}
}
va_end(ap); // 必須!
va_end(ap_backup); // コピーしたものも終了処理が必要
}
覚えておくべき制約(BUGSセクションより)
- 固定引数が最低1つ必要: void func(...) のような、固定引数がゼロの関数は作れません。必ず last となる引数が必要です。
- 対の原則: va_start(または va_copy)を呼んだら、必ず同じ関数内で va_end を呼んでください。
まとめ
可変長引数マクロは、printfのような引数がいくつ来るか事前にはわからない状況を柔軟に処理するために存在します。使い方は次のとおり。
- va_list 型の変数を用意する。
- va_start で開始位置を決める。
- va_arg で型を指定して中身を取り出す(ポインタも自動で進む)。
- va_end で締める。