可変個引数 | Programming Place Plus C言語編 第52章

トップページC言語編

このページの概要

以下は目次です。


可変個引数

これまで関数を自作する際、仮引数には void型または、1個以上の引数を書き並べました。そして、その関数を呼び出す際には、仮引数の型と個数に応じた実引数を渡さなければなりません。

ここで疑問になるのが、printf関数scanf関数のように、実引数の型も個数も一定でない関数の存在です。このような関数は、「引数が可変である」とか「可変個の引数を持つ」などといいます。

引数が可変である関数を宣言するには、以下のように書きます。

戻り値の型 関数名(仮引数の型 仮引数の名前, ...)

仮引数の並びの末尾にある「」が、引数が可変であることを表現します。

「…」は仮引数の並びの末尾に置かなければならず、その手前には最低でも1個は、void型以外の仮引数が必要です。「…」の部分は、いわばオプションの引数が並んでいることを示しています。この部分には、任意の型の引数が0個以上並んでいると考えられます。

有効な宣言と、エラーにある宣言を確認しておきます。

void f1(int num, ...);                   // OK
void f2(int num, const char* str, ...);  // OK
void f3(...);                            // エラー。... の手前に1個は仮引数が必要
void f4(void, ...);                      // エラー。void型とは両立しない
void f5(int num, ..., const char* str);  // エラー。... は末尾でなければならない
void f6(int num, ..., ...);              // エラー。... は複数回登場できない

ちなみに、printf関数と scanf関数の宣言は以下のようになっています。

int printf(const char* format, ...);
int scanf(const char* format, ...);

オプションの仮引数には名前が付いていないですし、型も分からないので、関数内でこれらの仮引数を使うためには特殊な操作が必要です。

そこで、stdarg.h で定義されている各種のマクロの助けを借ります。

次のサンプルプログラムでは、引数が可変の関数を定義し、標準出力へ任意の個数の値を出力しています。

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

void print(const char* format, ...);

int main(void)
{
    print("ddcd", 10, 20, 'x', 30);
    print("ss", "abc", "def");
    print("dfc", 50, 3.3f, 'Z');
}

/*
    標準出力へ任意の個数・型の値を出力する
    引数:
        format: 出力フォーマットを表す文字を並べたもの。
                d … 符号付き整数型
                f … 実浮動小数点型
                c … 文字型
                s … 文字列型
            とする。
            たとえば、"dds" と指定すると、
            後続の実引数が 整数型, 整数型, 文字列型 の順番で並んでいるものと判断される。
        ...:    出力する値のリスト
*/
void print(const char* format, ...)
{
    va_list args;
    va_start(args, format);

    for (const char* p = format; *p != '\0'; ++p) {
        switch (*p) {
        case 'd':
            printf("%d ", va_arg(args, int));
            break;
        case 'f':
            printf("%lf ", va_arg(args, double));
            break;
        case 'c':
            printf("%c ", va_arg(args, char));
            break;
        case 's':
            printf("%s ", va_arg(args, const char*));
            break;
        default:
            assert(!"不正な変換指定");
            break;
        }
    }
    printf("\n");

    va_end(args);
}

実行結果

10 20 x 30
abc def
50 3.300000 Z

print関数の内部を見てください。

まず、va_list型の変数を宣言しています。この型は、この後登場する各種のマクロで必要になる情報を保持するための専用の型です。具体的な内容は処理系定義ですし、特に知る必要もありません。

次に登場する va_startマクロは、可変個になっている部分の引数の取り扱いを開始することを意味しています。

va_startマクロには引数が2つあります。第1引数には、先ほどの va_list型の変数を、第2引数には、仮引数の並びで「…」の手前にある仮引数の名前を指定します。

次に、for文で、仮引数format を1文字ずつ調べています。これは printf関数の真似事のようなことをしており、‘d’、‘f’、‘c’、‘s’ の4つの文字にそれぞれ、符号付き整数型、実浮動小数点型、文字型、文字列型の意味を持たせています。これらの指定に応じて、可変部分の引数を1つ取り出し、その値をキャストして、標準出力へ出力します。

正確にいえばC言語には文字列型という型はありません。文字型の配列のことを指しています。

この過程の中で、va_argマクロが使われています。va_argマクロは、可変部分の引数を1つ返します。このマクロは使うたびに、返す引数が後ろへ移動します

どこまで返したかを覚えておくために、va_list型の変数があります。

va_argマクロの第1引数は、va_startマクロに指定した va_list型の変数を指定します。第2引数には、返してもらう引数の型を指定します。

va_argマクロの第2引数で、型を指定しなければならない点がポイントで、結局のところプログラマーは、実引数の型を知っていなければならないということです。このサンプルプログラムや printf関数、scanf関数のように、型情報を別の引数で表現させるようにするのが一般的です。

また、可変部分の引数の個数も分かっていないと、何回 va_argマクロを使えばよいのかも分かりません。このサンプルプログラムでは、出力フォーマットを表す引数に含まれる文字数から判断できます。

最後に、va_endマクロを使って、可変個の引数の処理を完了します。

va_endマクロの引数は1個だけで、va_list型の変数を指定します。va_startマクロと va_endマクロはきちんと対応付けて使用しないと、未定義の動作です。

なお、仮引数の型が不明なときに渡す実引数には、規定の引数の型拡張が行われ、実引数の型が暗黙的に変換されます。「…」はこれに該当します。この変換では、整数型には整数拡張(第21章)が、実浮動小数点型には float を double に拡張する変換が行われます。

このため、va_argマクロの第2引数に、型が拡張される前の型を指定すると正しく動作しません。たとえば、本来の実引数が「3.3f」という float型の値だと分かっているとしても、va_argマクロには double型であると伝えないといけません。

printf関数で double型の値を扱うときの変換指定が float型と同じ “%f” で構わないのに対し(“%lf” でもいいです)、scanf関数では “%lf” としなければならない(第20章)理由はここにあります。

printf関数で実浮動小数点型を扱う場合、実引数に float型と double型のどちらの値を渡すとしても、結局は暗黙的に double型に変換されるため、float と double を区別する意味がありません。そのためどちらの型の場合でも “%f” で扱えます。

long double型の場合は “%Lf” を使わないといけません。これは、暗黙的な型の拡張と関わっていないので、区別を付けなければならないためです。

一方、scanf関数で実浮動小数点型を扱う場合、実引数に指定するものは、float型や double型のポインタ型です。これは実浮動小数点型ではなくポインタ型なので、暗黙的な型の拡張とは無関係です。ポインタが指し示す先へ結果を代入するため、間違った大きさの値を代入しないように、変換指定を使い分けて、型の区別を付けなければなりません。

こちらも、long double型の場合は “%Lf” を使わないといけません。

va_list を利用した標準ライブラリ関数

今度は、自作のログ出力関数を作ってみましょう。printf関数と同じ形式で引数を渡すと、その内容を標準出力と、テキストファイルに同時に書き出すものとします。要するに、次の2つの文を1つにまとめた関数を作ります。

printf("value0: %d  value1: %d\n", value0, value1);
fprintf(fp, "value0: %d  value1: %d\n", value0, value1);

まず、可変個の引数に対応できないといけないことはいうまでもありません。とりあえず、思いつくままに書いてみます。

#include <stdio.h>
#include <stdlib.h>

void output_log(FILE* fp, const char* str, ...);

int main(void)
{
    FILE* fp = fopen("log.txt", "a");
    if (fp == NULL) {
        fputs("ファイルオープンに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    int value0 = -100;
    int value1 = 100;

    output_log(fp, "test message\n");
    output_log(fp, "value0: %d  value1: %d\n", value0, value1);

    if (fclose(fp) == EOF) {
        fputs("ファイルクローズに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
}

/*
    標準出力と、任意のファイルへ出力
    引数:
        fp:     出力先ファイルのポインタ。
        str:    出力するメッセージ。

    出力形式は、printf関数と同様。
    引数fp の指定に関わらず、標準出力へは出力される。
    引数fp がヌルポインタの場合は、標準出力にのみ出力する。
*/
void output_log(FILE* fp, const char* str, ...)
{
    va_list args;
    va_start(args, str);

    printf(str, args);

    if (fp != NULL) {
        fprintf(fp, str, args);
    }

    va_end(args);
}

実行結果

test message
value0: 4519504  value1: 4519752

このプログラムはコンパイルできますが、出力される結果が正しくありません。何か問題があるようです。

問題なのは、printf関数や fprintf関数に va_list型の変数 args を渡している点です。これらの関数の実引数はあくまでも、変換指定に従った型を持った値でなければならないのです。va_list型の変数を渡したからといって、そこから元の実引数一式が展開されるなどということはありません。

とはいえ、可変個の引数一式をほかの関数に引き渡すためには、va_list型の変数を使うしかありません。このような用途のために、標準ライブラリには、vprintf関数vfprintf関数といった関数が用意されています。

vprintf関数、vfprintf関数は、<stdio.h> に以下のように宣言されています。

int vprintf(const char* restrict format, va_list args);
int vfprintf(FILE* restrict fp, const char* restrict format, va_list args);

restrict については、第57章で取り上げます。動作に影響はないので、今は無視して問題ありません。

printf関数や、fprintf関数が仮引数に … を持っているのに対し、vprintf関数や vfprintf関数は va_list型の仮引数を持ちます。ですから、va_list型の変数を渡せます。これらの関数に置き換えてみましょう。

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

void output_log(FILE* fp, const char* str, ...);

int main(void)
{
    FILE* fp = fopen("log.txt", "a");
    if (fp == NULL) {
        fputs("ファイルオープンに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    int value0 = -100;
    int value1 = 100;

    output_log(fp, "test message\n");
    output_log(fp, "value0: %d  value1: %d\n", value0, value1);

    if (fclose(fp) == EOF) {
        fputs("ファイルクローズに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
}

/*
    標準出力と、任意のファイルへ出力
    引数:
        fp:     出力先ファイルのポインタ。
        str:    出力するメッセージ。

    出力形式は、printf関数と同様。
    引数fp の指定に関わらず、標準出力へは出力される。
    引数fp がヌルポインタの場合は、標準出力にのみ出力する。
*/
void output_log(FILE* fp, const char* str, ...)
{
    va_list args;

    va_start(args, str);
    vprintf(str, args);
    va_end(args);

    if (fp != NULL) {
        va_start(args, str);
        vfprintf(fp, str, args);
        va_end(args);
    }
}

実行結果 (標準出力)

test message
value0: -100  value1: 100

実行結果 (log.txt)

test message
value0: -100  value1: 100

今度は正しい結果を得られています。

ここで、vprintf関数や vfprintf関数を呼び出すたびに、va_startマクロと va_endマクロで囲んでいますが、これを次のように1つにしてしまうと、正しく動作しない可能性があります。

void output_log(FILE* fp, const char* str, ...)
{
    va_list args;
    va_start(args, str);

    vprintf(str, args);

    if (fp != NULL) {
        vfprintf(fp, str, args);  // 正しく動作しないかもしれない
    }

    va_end(args);
}

これは、規格上、vprintf関数や vfprintf関数といった va_list型の仮引数を持った標準ライブラリ関数を呼んだ後、渡した va_list型の変数の内容がどうなっているかを保証していないからです。ですから、これらの関数を複数呼び出す場合には、その都度、va_startマクロと va_endマクロで囲むようにして、毎回、可変個の引数の処理をやり直させる必要があります。

あるいは、va_copyマクロを使う方法があります。

void output_log(FILE* fp, const char* str, ...)
{
    va_list args, args2;

    va_start(args, str);
    va_copy(args2, args);

    vprintf(str, args);

    if (fp != NULL) {
        vfprintf(fp, str, args2);
    }

    va_end(args2);
    va_end(args);
}

va_copyマクロは、第2引数の va_list の内容を、第1引数の va_list へコピーします。

va_copy を使えば、2つ(あるいはそれ以上)の va_list を独立したものとして扱えますから、そのつど、va_startマクロと va_endマクロで囲むような対策が不要になります。

なお、va_copy で作られたコピーのほうに対しても va_end を適用する必要があるので、忘れないようにしてください。

va_copy は C99規格で追加されました。


最後に、printf関数、scanf関数系の関数を整理しておきます。

char型バージョン

wchar_t型バージョン

備考

… を使う

va_list型 を使う

… を使う

va_list型 を使う

printf

vprintf

wprintf

vwprintf

標準出力へ出力

fprintf

vfprintf

fwprintf

vfwprintf

任意のストリームへ出力

sprintf

vsprintf

swprintf

vswprintf

文字の配列へ出力。swprintf、vswprintf はバッファ長の指定が加わっている。

snprintf

vsnprintf

文字の配列へ出力。バッファ長指定版。wchar_t型版の名前に n は含まれない。

scanf

vscanf

wscanf

vwscanf

標準入力から入力

fscanf

vfscanf

fwscanf

vfwscanf

任意のストリームから入力

sscanf

vsscanf

swscanf

vswscanf

文字列から入力

かなり複雑です。また、似た名前で非標準の関数が用意されている環境もあるため、混乱に拍車がかかっています。記憶する必要はないので、使うときに調べればいいのですが、複雑さに対する覚悟が必要かもしれません。


可変個引数マクロ

関数形式マクロの引数も可変個にできます。

#include <stdio.h>

#define DEBUG

#ifdef DEBUG
#define PRINT(...)  fprintf(stderr, __VA_ARGS__)
#else
#define PRINT(...)  printf(__VA_ARGS__)
#endif

int main(void)
{
    const char* s = "abc";
    int n = 123;

    PRINT("%d\n", n);
    PRINT("%s %d\n", s, n);
}

実行結果

123
abc 123

関数形式マクロの定義の中で、引数が可変の部分に「…」を置きます。関数と違って、「…」は1個以上の引数を表し、「…」の手前に他の引数がなくても構いません。

置換後の並びについては、__VA_ARGS__と書いた部分が、可変個引数に指定した部分に対応し、実引数の内容によって(引数の区切りのコンマも含めて)置換されます。

たとえば以下の文は、

PRINT("%d\n", n);

以下のように置換されます。

fprintf(stderr, "%d\n", n);


練習問題

問題① 可変個引数で渡した int型整数の合計値を返す関数を作成してください。可変でない1個目の引数が、可変個部分の引数の個数を表すとします。たとえば、

total = sum(5, 10, -4, 7, -2, 9);

このように呼び出すと、変数total に 10 + (-4) + 7 + (-2) + 9 の結果である 20 が格納されるものとします。

問題② %d、%f、%c、%s の各変換指定子にだけ対応した、簡易的な printf関数を自作してください。“%3d” などの複雑な仕様は無視して構いません。また、実際に標準出力へ書き出す部分は、本物の printf関数を呼び出して構いませんが、vprintf関数は使わないでください。

問題③ 配列へ要素をまとめて格納する関数を作成してください。たとえば、

assign(array, 5, 0, 1, 2, 3, 4);

このように呼び出すと、int型で要素数が 5 の配列array に、0, 1, 2, 3, 4 という値を順番に格納するものとします。


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



前の章へ (第51章 日付と時間)

次の章へ (第53章 再帰呼び出し)

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る