WisdomSoft - for your serial experiences.

9.2 可変個引数

printf() 関数のような異なる数のパラメータを受け取ることができる関数を作成する方法を解説します。

9.2.1 省略記号と可変個引数並び

これまでの解説で、C言語の基本的な構文や仕様は網羅してきましたが、それでも printf() 関数のようなものを作ることを考えると、ある疑問が浮上します。printf() 関数や scanf() 関数のように、C言語の標準関数には引数を動的に取得することができるものが存在します。例えば printf() 関数を見た場合、printf("...") でも、printf("..." , variable) でもコンパイルすることができます。このような汎用的な関数はどうやって作るのでしょうか。

引数を可変個数受け取ることができる動的な引数を実現するには、3つのピリオドで構成される省略記号 "..." を用います。可変個の引数を取る関数の代表例である printf() 関数は次のように宣言されています。

printf() 関数
int printf( const char *format ,...);

これは、第1引数の format には必ず書式制御文字列を指定しなければなりませんが、第2引数以降は自由に指定することができるということを表しています。このように、省略記号は引数の最後に指定することができます。省略記号の後に明示的な引数が現れてはいけません。

これで、可変個の引数を受け取ることができるようにはなりましたが、問題は関数本体で引数並びを取得する方法です。引数の数や型は、関数が呼び出されるまで不明なので、パラメータとして受け取ることはできません。そこで、可変個引数並びを取得するには C 言語が提供する標準ライブラリで定義されているマクロ関数を利用します。

引数の取得には、va_start() マクロ関数va_arg() マクロ関数va_end() マクロ関数の3つのマクロ関数を用います。これらのマクロを利用するには stdio.h ヘッダ及び stdarg.h ヘッダファイルをインクルードしなければなりません。

va_start() マクロ関数
void va_start( va_list arg_ptr, prev_param );
va_arg() マクロ関数
type va_arg( va_list arg_ptr, type );
va_end() マクロ関数
void va_end( va_list arg_ptr );

全てのマクロ関数で指定する arg_ptr という引数は va_list 構造体の変数です。va_list 構造体は stdarg.h で宣言されている型で、マクロの情報源となります。開発者はこの構造体の定義について知る必要はありません。この変数はマクロが使用するものであり、開発者がメンバを直接操作することは許されないためです。

まず、最初に va_start() マクロ関数を呼び出して va_list 構造体変数を初期化しなければなりません。第2引数の prev_param には可変個引数並びの直前のパラメータの名前を指定します。例えば printf() 関数の宣言であれば、省略記号の直前のパラメータである format をここに指定します。prev_param が register 記憶クラスで宣言されている場合は、このマクロの動作は未定義です。

prev_param に指定するパラメータは関数の定義によって変化します。マクロ関数は指定されたパラメータの次からが取得するべき可変個引数並びだと判断するのです。次のような関数定義の時、va_start() の prev_param に指定するパラメータはこのようになるでしょう。

void Function(int iValue , ...) → va_start(arg_ptr , iValue)

void Function(int iValue , char * pStr , ...) → va_start(arg_ptr , pStr)

初期化が終われば va_arg() マクロ関数で引数を得ることができます。第2引数の type には引数の型を指定してください。このマクロ関数は va_list 型構造体変数の情報を元に指定した型の値を取得します。同時に、マクロは type で指定された型情報から va_list が示す引数位置を移動させ、次の引数が始まる位置を指すように処理します。すなわち va_arg() マクロ関数を呼び出すたびに次の引数を得ることができるのです。

va_arg() マクロ関数を利用するためには、可変個引数並びに与えられた引数の数と、引数の型を知る必要があります。printf() 関数は書式制御文字列から引数の数と型を認識していたのです。例えば可変個引数並びに浮動小数と文字列へのポインタが順に与えられている場合、次のように取得することができます。

fValue = va_arg(arg_ptr , float);
pStr = va_arg(arg_ptr , char *);

最後に、処理が終了すれば va_end() マクロを使って arg_ptr を解放します。これで、可変個数の引数を取得するための一連の処理が終了します。これらの3つのマクロを使うことで、動的な引数にアクセスすることができるのです。

コード1
#include <stdio.h>
#include <stdarg.h>

void DynamicParameter(int arg_num , ...) {
	va_list args;
	int iValue , iCount;

	if (arg_num < 1) return;

	va_start(args , arg_num);

	for (iCount = 0 ; iCount < arg_num ; iCount++) {
		iValue = va_arg(args , int);
		printf("第%d引数 = %d\n" , iCount + 2 , iValue);
	}

	va_end(args);
}

int main() {
	DynamicParameter(4 , 10 , 20 , 30 , 40);
	return 0;
}
実行結果
コード1 実行結果

コード1では、可変個数の引数を受ける関数 DynamicParameter() 関数を作成しています。この関数は第1引数にそれ以降のオプション引数の個数を指定します。動的な引数では va_arg() マクロを何度呼び出すべきかを決定しなければならないので、引数の数を知る必要があります。printf() 関数などでは書式制御文字列から引数の数を分析しています。

DynamicParameter() では可変個引数を得るために、arg_num からその後の引数の数を取得しています。arg_num が 1 以下であればそれ以外の引数がないことを表すので、何もせずに制御を返します。最初の va_start() では va_list を初期化し、その後 for ループで va_arg() から数値型として引数値を受け取っています。

このような、動的に引数を得る関数は極めて汎用的な機能を提供することができるようになりますが、引数の解析や複雑な機能の提供は必要以上の計算を必要とするため処理に負担がかかるという問題もあります。そのため、こうした関数を作るのはごく稀であると考えてよいでしょう。