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

この章の概要

この章の概要です。

可変個引数

関数には引数を渡すことができます。

自作関数を作るとき、仮引数がなければ void型を指定し、あれば int型などの型を必要な個数分だけ書き並べていました。 関数を呼び出すときには、その関数の仮引数がどうなっているかに応じて、適切な型と個数の実引数を、順番通りに指定しなければいけません。

これは標準ライブラリ関数でも同様ですが、 ここで printf関数(⇒リファレンス)や scanf関数(⇒リファレンス) のような謎めいた仕様の関数があることに気が付きます。 printf関数は、フォーマット指定の仕方によって、実引数の型も個数も変化します。 これをどうやって実現しているのでしょうか?
ここでのテーマである可変個引数が、実現の鍵を握っています。 これを理解すると、自作関数でも、printf関数のように、引数の型や個数を自由に変化させられます。


まず、printf関数のプロトタイプ宣言をお見せしましょう。

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

実は、戻り値が存在していることが分かりますが、これはここでは関係ないので無視しておくとして、 肝心の可変個引数を実現する鍵は、2つ目の仮引数 ... にあります。

ちなみに戻り値は、関数が失敗したときに EOF(⇒リファレンス)を返します。 しかし、printf関数の戻り値をいちいちチェックするプログラムは、まず見かけることはありません。

仮引数に ... を指定すると、その部分には任意の型、任意の個数の仮引数が存在するという意味になります。
なお、... の手前には、必ず1つは明確な仮引数が存在していなければならず、... は最後の仮引数でならないという2つのルールがあります。 したがって、以下の宣言はいずれも不正です。

void func_a(...);                            /* ... だけではダメ */
void func_b(int num, ..., const char* str);  /* ... は最後に来ないとダメ */
void func_c(int num, ..., ...);              /* ... が複数回登場してはダメ */

仮引数の指定の仕方はこれだけですが、問題はこのように宣言された関数の中身の方です。 通常の固定的な引数と違い、それぞれに名前も付いていませんし、型がどうなっているかも分かりません。個数だって不明です。
そこで、標準ライブラリに含まれている 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マクロが現れます。 第1引数に、先ほどの va_list型の変数を指定します。 第2引数には、可変個引数の1つ手前にある仮引数の名前を指定します。
今回の場合、print(const char* format, ...); という関数なので ... の手前に来る format を指定します。 func(int a, int b, ...); のような関数なら b を指定するということです。
このマクロを呼び出したら、可変個引数の解析が開始できるようになります。

仮引数format は、簡易的なフォーマット指定文字列です。 仕様はコメントにある通りです。 format の内容を1文字ずつ確かめながら、va_argマクロを介して、printf関数に値を渡しています。
va_argマクロは、次の可変個引数を1つ返します。 この「次の」が、実際にどれなのかを管理するために va_list型の変数があります。 要するに、... の部分に指定した実引数を、手前側から1つずつ順番に参照していくということです。
第1引数には、va_list型の変数を指定し、 第2引数には、次の可変個引数が実際にはどんな型であるかを指定します。
結果として、次の可変個引数が指定された型で参照され、その値が返されます。

C99 では、va_copyマクロが追加されており、解析途中の状態のコピーを取っておけるようになりました。 これにより、現時点までに解析し終えた部分を後戻りして参照できるようになっています。

最後に、va_endマクロを呼び出して、可変個引数の解析を終了します。


ここで注意が必要なのは、... の部分に実引数を渡すときに、引数省略時の型の格上げが働く点です。 これは、可変個引数については、int型よりも小さい整数型は int型に、float型は double型へ変換されるというものです。
このため、va_argマクロの第2引数に short型や float型 を指定すると正しく動作しません。 先ほどのサンプルで言えば、実引数として float型の 3.3f を渡していても、double型として参照しているのはこのためです。

scanf関数で double型を受け取るときには "%lf" を指定するのに、printf関数で double型を出力するときには "%f" を指定するのは、 この引数省略時の型の格上げに理由があります。
printf関数の場合、その内部で va_argマクロを呼ぶ際、どのみち double型として参照するしかありませんから、常に "%f" を指定します。
scanf関数の場合は、... の部分に指定される実引数は、常にポインタ型ですから、そのポインタがどんな型を指しているのかを厳密に指定する必要があります。 本当は float型変数を指すポインタを渡したのに、scanf関数が double型のつもりで代入を行うようなことが起きてはならないのです。

とはいえ分かりにくいので、printf関数でも "%lf" で double型を表現できる環境もあります。 しかしそれは標準規格で保証されたものではないので、他の環境に移るとうまく動作しなくなる可能性があります。

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

可変個引数の部分は、関数同様 ... で表現します。 可変でない部分があれば、通常の関数形式マクロと同様に引数名を並べてから(マクロなので型名はありません)、末尾に ... を置きます。

置換後の並びについては、__VA_ARGS__ と書いた部分が、可変個引数の部分に指定した部分と対応します。

可変個引数マクロは、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます

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

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

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

こういう2つの関数呼び出しを、1つにまとめた関数を作ります。

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

#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 が NULL の場合は、標準出力にのみ出力する。
*/
void outputLog(FILE* fp, const char* str, ...)
{
	printf( str, ... );       /* 可変個引数をどう渡す? */

	if( fp != NULL ){
		fprintf( fp, str, ... );   /* 可変個引数をどう渡す? */
	}
}

このままではコンパイルできません。 問題は、本物の printf関数や fprintf関数(⇒リファレンス)に、可変個引数をどう渡せばいいかです。 このプログラムのように、実引数のところに ... を書いてもダメです。

前の項で見たように、可変個引数を使う際には、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 が NULL の場合は、標準出力にのみ出力する。
*/
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

これでコンパイルは通りました。 しかし、実行結果を見ると分かるように、出力される値は正しくありません。
前の項で、va_list型の変数を扱ったとき、va_argマクロを使って可変の部分の引数を参照しました。 可変個引数を正しく参照するには、va_argマクロを使う以外に方法はなく、va_list型のまま printf関数などに渡しても、意図したようには扱ってくれません。

正しい結果を得るには、printf関数の代わりに vprintf関数(⇒リファレンス)を、 fprintf関数の代わりに vfprintf関数(⇒リファレンス)を使います。

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 が NULL の場合は、標準出力にのみ出力する。
*/
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マクロで囲むべきです。

C99 で新たに追加された va_copyマクロを使う方法もあります。


このように、... を持った標準関数には、va_list型を渡せる別バージョンが用意されています。 printf関数、scanf関数系の関数を整理しておきます。

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

命名規則が完全に統一されている訳でもないですし、似た名前の非標準関数を持った環境もあり、非常に複雑です。 記憶する必要はないので、使うときに調べればいいでしょう。

C99 対応を謳っていない環境でも、C99規格で追加された関数が使える場合もあります。


練習問題

問題@ 可変個引数で渡した int型整数の合計値を返す関数を作成して下さい。例えば、

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

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

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

問題B 配列へ要素をまとめて格納する関数を作成して下さい。例えば、

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

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


解答ページはこちら

参考リンク

更新履歴

'2017/3/25 VisualC++ 2017 に対応。

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2015/10/12 clang の対応バージョンを 3.4 に更新。

'2015/9/5 VisualC++ 2012 の対応終了。

'2015/8/29 fopen関数、flose関数の戻り値をチェックするようにした。

'2015/8/18 VisualC++ 2010 の対応終了。

'2015/8/15 VisualC++ 2015 に対応。

'2014/10/18 clang 3.2 に対応。

'2014/2/1 VisualC++ 2013 に対応。

'2014/1/14 VisualC++ 2008 の対応終了。

'2014/1/12 clang 3.0 に対応。

'2013/4/17 C99 の可変個引数マクロについての項を追加。

'2012/2/28 サンプルプログラム内の outputLog関数が、環境によっては正しく動作していなかったのを修正。

'2011/4/30 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ