C言語の可変引数リスト「. . .」の使い方

C言語には可変引数リスト「. . .」というものがあります。これは関数のパラメータが不定であるときに使われます。例えばfuncという関数が

void func( int a, . . . );

と宣言されていた場合、func関数を呼び出すほうは func( 1, 100 ); でも良いし、func( 2, 100, 200 ); でも良いです。第一パラメータに可変引数リストの個数を指定することで任意の個数のint型パラメータを渡すことができます。この実装方法についてサンプルプログラムをもとに説明してみようと思います。可変引数リストはパラメータの個数だけでなく型まで任意にすることができるので、その例を出します。なお呼び出されるfunc関数の「. . .」は引数の個数も型も不定ですので「. . .」に該当する引数を「名なし引数」と呼ぶことにします。

実際のプログラムソースを示す前に要点を書くと、以下です。func関数側の実装です。

  • 名なし引数を次々に参照する「va_list」という型の変数を宣言する。
  • その変数が最初の名なし引数を指すように「va_start」という関数を呼ぶ。
  • 変数が次の名なし引数を指すように「va_arg」という関数を呼ぶ。
  • 最後の名なし引数までたどり着いたら「va_end」という関数を呼ぶ。

va_start、va_arg、va_end は、実際はマクロとして定義されています。

サンプルのプログラムソース示して使い方を書いてみます。


コピペするなら以下を使ってください。


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

void func( int n, char *p, ... );

int main( void )
{
	int	i;
	char	a[10];

	/* 可変引数の個数は2つ */
	i = 2;

	/* 1つ目の引数はchar型、2つ目の引数はint型 */
	a[0] = 'c';
	a[1] = 'i';

	/* 可変引数の数を2つで呼ぶ */
	func( i, a, 'A', 10 );

	/* 可変引数の個数は3つ */
	i = 3;

	/* 1つ目の引数はint型、2つ目の引数はfloat型、3つ目の引数は文字列 */
	a[0] = 'i';
	a[1] = 'f';
	a[2] = 's';

	/* 可変引数の数を3つで呼ぶ */
	func( i, a, 20, 0.234, "Hello." );

	return 0;
}

void func( int n, char *p, ... )
{
	/* ループカウンタ用 */
	int i;

	/* 可変引数リストの値を格納する変数 */
	char cval;
	int ival;
	float fval;
	char *sval;

	/* 名なし引数を順番に指すようにするポインタ */
	va_list	ap;

	/* apを最初の名なし引数を指すようにする */
	va_start( ap, p );

	for( i = 1; i <= n; i++ )
	{
		printf( "%dつ目の引数: ", i);

		switch( *p++ )
		{
			case 'c':
				cval = va_arg( ap, int );
				printf( "%c\n", cval );
				break;
			case 'i':
				ival = va_arg( ap, int );
				printf( "%d\n", ival );
				break;
			case 'f':
				fval = va_arg( ap, double );
				printf( "%f\n", fval );
				break;
			case 's':
				for( sval = va_arg( ap, char *); *sval; sval++ )
					putchar( *sval );
				printf( "\n" );
				break;
		}
	}

	/* クリーンアップ */
	va_end( ap );
}

このプログラムはメイン関数から可変引数リストを持つfunc関数を呼び出しています。func関数の定義は

void func( int n, char *p, . . . );

となっていて、第一パラメータはintで可変引数リストの個数を設定します。第二パラメータはchar型配列へのポインタで、ここに「どんな型を可変引数リストに設定しているか」の情報を設定します。今回のサンプルソースの例では配列に設定する情報を以下のようにしています。

パラメータがchar型・・「c」を設定
パラメータがint型・・「i」を設定
パラメータがfloat型・・「f」を設定
パラメータが文字列・・「s」を設定

呼び出されたfunc関数が可変引数リストを間違いなくたどれるように、呼び出し元でパラメータの個数と型の情報を渡しているわけです。

メイン関数ではfunc関数を2回呼んでいますが、それぞれ以下のようにして呼び出しています。

/* 可変引数の数を2つで呼ぶ */
func( i, a, ‘A’, 10 );

可変引数リストの1つ目は文字の「A」、2つ目は数字の「10」、可変引数リストの個数は2個。

/* 可変引数の数を3つで呼ぶ */
func( i, a, 20, 0.234, “Hello.” );

可変引数リストの1つ目は数字の「20」、2つ目は小数の「0.234」、3つ目は文字列で「Hello.」、可変引数リストの個数は3個。

それでは、func関数側がどのように可変引数リストをたどるかを見てみます。

[広告]

func関数では可変引数リストの値を格納する変数と、va_list型の変数 ap を定義します(ap は argument pointer の略です)。そして ap が最初の名なし引数を指すように va_start を以下のように呼んでいます。

/* apを最初の名なし引数を指すようにする */
va_start( ap, p );

va_start の第二パラメータはfunc関数から渡される名前のある引数の最後のものを指定します。今回の例でいうと

void func( int n, char *p, . . . );

であるため、n と p が名前のある引数になります。このうち最後のものは後ろにある p のほうです。va_start に名前のある最後の引数を与えることによって ap が(次に位置する)最初の可変引数リストの名なし引数を指すようになるわけです。これはつまり、可変引数リストを使うには名前のある引数が少なくとも1つは必要ということを言っています。

これで ap が名なし引数を指すようになったので、あとは1つづつ名なし引数をたどっていきます。名なし引数をたどるには va_arg を使います。va_arg の使い方は

取得する名なし引数 = va_arg( ap, 次に進める分の型 );

です。va_arg の戻り値から名なし引数の値を取得できます。戻り値の型は都度変わってくるためサンプルプログラムではswitch文でケース分けをして取得しています。「次に進める分の型」というのは「どれくらい大きく ap を進めるか」ということを意味します。メイン関数から名なし引数の型の情報をもらっているため va_arg でこれを設定することができます。名なし引数がint型だった場合は

ival = va_arg( ap, int );

です。「次に進める分の型」にはintを指定します。なお、名なし引数が文字列の場合はfor文を使ってNULL文字が来るまでループさせています。

ちなみにここでやってみて気づいたことを1つ書いておきます。

名なし引数の型がchar型とfloat型のときですが以下のようにやるとコンパイル時に警告になります(というか、なりました)。

cval = va_arg( ap, char );
fval = va_arg( ap, float );

警告メッセージは以下です。

warning: ‘char’ is promoted to ‘int’ when passed through ‘. . .’
warning: ‘float’ is promoted to ‘double’ when passed through ‘. . .’

名なし引数がchar型の場合、次の名なし引数に進める増分はint型と同じ。float型の場合の増分はdouble型と同じということらしいです。なのでサンプルプログラムでは以下としています。

cval = va_arg( ap, int );
fval = va_arg( ap, double );

最後に va_end を呼んで ap のクリーンアップをして終了します。

このサンプルプログラムの結果は以下となります。

$ ./a.out
1つ目の引数: A
2つ目の引数: 10
1つ目の引数: 20
2つ目の引数: 0.234000
3つ目の引数: Hello.

スポンサーリンク

ポインタ変数をconst指定したときの動きについて

ポインタ変数をconstで修飾する方法ですが、以下の2つがあります。

int i = 100;
const int *cp1 = &i;
int *const cp2 = &i;

この違いですが、cp1 は「const int」へのポインタであるため cp1 を使って cp1 が指す値を変更することはできません。上の例では「*cp1 = 300」とするのは間違いです。ですが、cp1 には他のポインタアドレスを代入することはできます。

cp2 はアドレスの変更ができません。cp2 は常に変数iのアドレスを指します。ですが、cp2 を使って変数iの値を変更することはできます。

これらの動きを試したソースコードを載せておきます。

コピペ用です。


#include <stdio.h>

int main( void )
{
	int i = 100;
	int j = 200;

	const int *cp1 = &i;
	int *const cp2 = &i;

	printf( "1: %d\n", *cp1);

	/* cp1を使ってiは変更できないのでcp2を使う */
	*cp2 = 300;
	printf( "2: %d\n", *cp1 );

	/* cp2のポインタの向き先は変えられないがcp1は変えられる */
	cp1 = &j;
	printf( "3: %d\n", *cp1 );

	return 0;
}

実行結果は以下となります。

$ ./a.out
1: 100
2: 300
3: 200

ちなみに文字配列にconstを使用した以下の例は、どうなるかわかるでしょうか?

const char a[ ] = “Apple”;
char *const p = “Orange”;

Appleという文字列は変更できなくなります。ポインタ p の向き先を変更することもできません。またOrengeという文字列を変更しようとすると不定になります。これについてはこの記事を見てみてください。

スポンサーリンク