C言語編 第52章 可変個引数

先頭へ戻る

この章の概要

この章の概要です。


可変個引数

これまで関数を自作する際、仮引数には 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' );

    return 0;
}

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

    va_start( args, format );

    for( p = format; *p != '\0'; ++p ){
        switch( *p ){
        case 'd':
            printf( "%d ", va_arg(args, int) );
            break;
        case 'f':
            printf( "%f ", 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つ取り出し、その値をキャストして、標準出力へ出力します。

この過程の中で、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関数や scanf関数で、double型の値を扱うときの変換指定が、printf関数では float型と同じ "%f" なのに、scanf関数では "%lf" としなければならない(第20章)理由はここにあります。

printf関数で浮動小数点型を扱う場合、実引数に float型や double型の値を渡しています。どちらの型を渡したとしても、暗黙的に double型に変換されます。そのため、printf関数では 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 outputLog(FILE* fp, const char* str, ...);

int main(void)
{
    int value0, value1;
    FILE* fp;


    value0 = -100;
    value1 = 100;

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

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

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

    return 0;
}

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

    出力形式は、printf関数と同様。
    引数fp の指定に関わらず、標準出力へは出力される。
    引数fp がヌルポインタの場合は、標準出力にのみ出力する。
*/
void outputLog(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* format, va_list args);
int vfprintf(FILE* fp, const char* format, va_list args);

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

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

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

int main(void)
{
    int value0, value1;
    FILE* fp;


    value0 = -100;
    value1 = 100;

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

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

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

    return 0;
}

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

    出力形式は、printf関数と同様。
    引数fp の指定に関わらず、標準出力へは出力される。
    引数fp がヌルポインタの場合は、標準出力にのみ出力する。
*/
void outputLog(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 outputLog(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_list型を渡す別バージョンが用意されています。printf関数、scanf関数系の関数を整理しておきます。

char型バージョン wchar_t型バージョン 備考
... を使う va_list型 を使う ... を使う va_list型 を使う
printf vprintf wprintf
vwprintf
標準出力へ出力
fprintf vfprintf fwprintf
vfwprintf
任意のストリームへ出力
sprintf vsprintf swprintf
vswprintf
文字の配列へ出力。swprintf、vswprintf はバッファ長の指定が加わっている。
snprintf
(C99以降)
vsnprintf
(C99以降)
文字の配列へ出力。バッファ長指定版。wchar_t型版の名前に n は含まれない。
scanf vscanf
(C99以降)
wscanf
vwscanf
(C99以降)
標準入力から入力
fscanf vfscanf
(C99以降)
fwscanf
vfwscanf
(C99以降)
任意のストリームから入力
sscanf vsscanf
(C99以降)
swscanf
vswscanf
(C99以降)
文字列から入力

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


C99 (可変個引数マクロ)

C99

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

#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 );

    return 0;
}

実行結果

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 という値を順番に格納するものとします。


解答ページはこちら

参考リンク



更新履歴

'2018/4/20 「NULL」よりも「ヌルポインタ」が適切な箇所について、「ヌルポインタ」に修正。

'2018/3/27 全面的に文章を見直し、修正を行った。

'2018/2/1 C言語編全体で表記を統一するため、「フォーマット指定」を「変換指定」に改めた。

'2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。

'2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

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



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

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

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

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー