本文へスキップ
BecomeCoder

C言語コース · 第10章 関数ポインタと標準ライブラリ活用 · レッスン41

可変長引数(va_list) ― printf的な独自関数を自作する

ブラウザで完結

導入

printf("%d\n", x) のように1つだけ値を渡しても、printf("%d %d %d\n", a, b, c) のように3つ渡しても、同じ printf がコンパイルできてしまいます。関数の引数の数が呼び出しごとに変わる――これを実現しているのが可変長引数という仕組みです。stdarg.h を使えば、自分の関数でも同じことができます。

説明

可変長引数を受け取る関数は、決まった引数のあとに ... を書きます。中身を読み取るには <stdarg.h> の3つの道具を使います。

  • va_list … 「今どこまで読んだか」を覚えておく特別な型。
  • va_start(リスト, 最後の名前付き引数) … 読み取りを開始する準備。
  • va_arg(リスト, 型) … 次の引数を指定した型として読み取り、読み取り位置を1つ先に進める。
  • va_end(リスト) … 読み取り終了の後始末。
#include <stdio.h>
#include <stdarg.h>

int sum_all(int count, ...) {
    va_list args;
    va_start(args, count);

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);
    }

    va_end(args);
    return total;
}

int main(void) {
    printf("合計: %d\n", sum_all(3, 10, 20, 30));
    printf("合計: %d\n", sum_all(5, 1, 2, 3, 4, 5));
    return 0;
}

sum_all は、関数自身では「いくつ引数が渡されたか」も「その型が何か」も知る手段がありません。そこで最初の引数 count に「これから何個読むか」を渡してもらい、va_start(args, count) で「count の次から可変長引数が始まる」と教えています。va_arg(args, int) を呼ぶたびに、次の引数を int として1つ読み取り、読み取り位置が自動的に進みます。

flowchart LR
  s["va_start(args, count)<br/>読み取り開始位置を決める"] --> a1["va_arg(args, int) 1回目"]
  a1 --> a2["va_arg(args, int) 2回目"]
  a2 --> dots["…count回くり返す"]
  dots --> e["va_end(args)"]

printf が「何個・どんな型の引数が来るか」を知っているのは、書式指定子(%d%s)を先頭から数えているからです。同じ考え方で、書式文字列を自分で走査する簡易版 my_printf も作れます。

#include <stdio.h>
#include <stdarg.h>

void my_printf(const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);

    for (const char *p = fmt; *p != '\0'; p++) {
        if (*p == '%' && *(p + 1) == 'd') {
            printf("%d", va_arg(args, int));
            p++;
        } else if (*p == '%' && *(p + 1) == 's') {
            printf("%s", va_arg(args, const char *));
            p++;
        } else {
            putchar(*p);
        }
    }

    va_end(args);
}

int main(void) {
    my_printf("名前: %s, 年齢: %d\n", "Taro", 20);
    return 0;
}

この回はプリプロセッサ非対応のJSCPPではなく、本物のCコンパイラで実行されます。標準入力(scanf)は使わず、値はコードの中に直接書いてください。

やってみよう

sum_all に渡す個数や値を変えて呼び出してみましょう。my_printf の呼び出しの引数(名前・年齢)を書き換えて、結果が変わることも確かめてください。

演習

可変長引数を受け取り、渡された整数の中から最大値を返す関数 int max_of(int count, ...) を書いてください。max_of(4, 3, 9, 2, 7) の結果を printf で表示してください(結果は 9)。

ヒント1を見る

va_list args; va_start(args, count); から始め、forcountva_arg(args, int) を読みます。

ヒント2を見る

最初の値を仮の最大値にしておき、if (v > best) best = v; のように更新していきます。

実際に動かしてみよう

下のエディタにCを書いて「コンパイル & 実行」を押すと、ブラウザ内で本物のCコンパイラ(clang + dlmalloc)がその場でコンパイルして実行し、出力が表示されます。構造体・共用体・malloc/free・関数ポインタ・ファイル入出力などがそのまま動きます。本文の例を書き換えて試しながら進めましょう(初回だけコンパイラの読み込みに時間がかかります。以降はキャッシュされます。この回は標準入力(scanf)は使わず、値はコードに直接書きます)。

C/C++ — ブラウザ内で本物のコンパイラで実行

ブラウザ内で本物のC/C++コンパイラ(clang + libc++ / WebAssembly)を動かします。構造体・malloc/free・std::string・vector・クラス・テンプレート・STL などがそのまま実行できます(初回のみコンパイラの読み込みに時間がかかります。以降はブラウザにキャッシュされます)。
スクロールして表示された時点でも自動で読み込まれます。