15章.可変個の引数リスト



15.1:
printf()を呼ぶ前に必ず<stdio.h>を必ず#includeするようにと言わ れた。なぜ?

A:

printf()の正しいプロトタイプが有効範囲(スコープ)に入るようにす るためにである。

コンパイラは可変個の引数並びを取る関数には別の呼び出す仕組み を使っているかもしれない。(そうするのは固定長の引数並びの関数 で使っているのより効率の悪い呼び出しかたになっているからかもし れない。) よってプロトタイプは(省略記号「...」を使って引数並び が可変長であることを示すことで)、可変長の引数並びを使うときは いつも可変個の引数用の呼び出す仕組みを使う必要があるとコンパイ ラに分かるように、有効範囲に入るようにしなければならない。

References:
ANSI Sec. 3.3.2.2, Sec. 4.1.6; ISO Sec. 6.3.2.2, Sec. 7.1.7; Rationale Sec. 3.3.2.2, Sec. 4.1.6; H&S Sec. 9.2.4 pp. 268-9, Sec. 9.6 pp. 275-6.


15.2:
どうして%fをprintf()のfloatの変数にもdoubleの変数にも使うこと ができるのか。この2つは別のデータ型ではないのか。

A:

可変長引数並びの可変長の部分には「省略時の引数の格上げ(default argument promotions)」が適用される。charとshort intはintに、 floatはdoubleにそれぞれ格上げされる(これはプロトタイプが有効範 囲外の場合に適用される格上げと同じものである。これは"古い書き 方"として知られている。質問11.3を参照のこと)。したがってprintf の書式%fに見えるのはいつもdoubleの数である(同様に%cが見るのは、 %hdと同じく、intの数である)。質問12.912.13も参照のこと。

References:
ANSI Sec. 3.3.2.2; ISO Sec. 6.3.2.2; H&S Sec. 6.3.5 p. 177, Sec. 9.4 pp. 272-3.


15.3:
解けなくてイライラしていた問題が実は、

	printf("%d", n);

で起きていることがわかった。ANSIの関数プロトタイプは上のような 引数の型の不一致から守ってくれると思っていたのだが。

A:

関数が可変数個の引数を取るとき、プロトタイプは可変数個の引数の 数やそのデータ型についての情報を与えない(そもそも出来ない)。よっ て通常の保護の仕組みは、可変数個の引数リストの可変長の部分には あてはまらない。コンパイラは暗黙の変換も(通常の)データ型の不 一致も警告もできない。

質問5.2, 11.3, 12.9, 15.2も参照のこと。


15.4:
可変個の引数を取る関数をどうやって書けばよいか。

A:

<stdarg.h>にある機能を使う。

以下はmallocされた記憶領域に任意の個数の文字列を連結して格納す る関数である。

                
	#include <stdlib.h>		/* for malloc, NULL, size_t */
	#include <stdarg.h>		/* for va_ stuff */
	#include <string.h>		/* for strcat et al. *

	char *vstrcat(char *first, ...)
	{
		size_t len;
		char *retbuf;
		va_list argp;
		char *p;

if(first == NULL) return NULL;

len = strlen(first);

va_start(argp, first);

while((p = va_arg(argp, char *)) != NULL) len += strlen(p); va_end(argp); retbuf = malloc(len + 1); /* おしりの\0用に+1 */ if(retbuf == NULL) return NULL; /* エラー */ (void)strcpy(retbuf, first); va_start(argp, first); /* 2回目のスキャンのためにリスタート */ while((p = va_arg(argp, char *)) != NULL) (void)strcat(retbuf, p); va_end(argp); return retbuf; }

使い方は以下のようになる。

	char *str = vstrcat("Hello, ", "world!", (char *)NULL);

最後の引数をキャストする必要があることに注意。質問5.215.3参 照(使う人が、返ってきたmallocされた領域を解放する必要があるこ とにも注意)。

ANSI規格以前に開発されたコンパイラを使うときは、まず関数定義を プロトタイプなしに書き換え("char *vstrcat(first) char *first; {")、次に<stdlib.h>の代わりに<stdio.h>を#includeして、"extern char malloc();"を追加する。またsize_tの代りにintを使う。(void) のキャストを取り除いたり、stdargではなく古いvagrgsのパッケージ を使わなければならないかもしれない。質問15.7も参照のこと。

References:
K&R2 Sec. 7.3 p. 155, Sec. B7 p. 254; ANSI Sec. 4.8; ISO Sec. 7.8; Rationale Sec. 4.8; H&S Sec. 11.4 pp. 296-9; CT&P Sec. A.3 pp. 139-141; PCS Sec. 11 pp. 184-5, Sec. 13 p. 242.


15.5:
printf()のように書式文字列と可変個の引数をもらって、引数を printf()に渡して、仕事のほとんどprintfにやらせるような関数を、 どうやって書けばよいのか。

A:

vprintf()かvfprintf()かvsprintf()を使え。

「error: 」という文字列を先頭にエラーメッセージを出力し、改行 で終わる関数error()を以下に示す。

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

	void error(char *fmt, ...)
	{
		va_list argp;
		fprintf(stderr, "error: ");
		va_start(argp, fmt);
		vfprintf(stderr, fmt, argp);
		va_end(argp);
		fprintf(stderr, "\n");
	}
質問15.7も参照のこと。

References:
K&R2 Sec. 8.3 p. 174, Sec. B1.2 p. 245; ANSI Secs. 4.9.6.7,4.9.6.8,4.9.6.9; ISO Secs. 7.9.6.7,7.9.6.8,7.9.6.9; H&S Sec. 15.12 pp. 379-80; PCS Sec. 11 pp. 186-7.


15.6:
scanf()に類似の関数を書いて、scanf()を呼んでほとんどの仕事をさ せたい。どうすればいいか。

A:

残念ながら、vscanfのたぐいは標準ではない。自分で書くしかない。


15.7:
ANSI規格決定前に開発されたコンパイラを使っているが、 <stdarg.h>がない。どうすればいいか。

A:

古いヘッダー<varargs.h>というのがあって、これがほとんど 同じ機能を提供する。

質問15.5のerror()関数を書き直して<varargs.h>を使うようにするに は、関数ヘッダーを、

	void error(va_alist)
	va_dcl
	{
		char *fmt;

に変え、va_startの行を、

	va_start(argp);
に変え、そして、

	fmt = va_arg(argp, char *);
をva_start()を呼ぶところとvfprintf()を呼ぶところの間に加える。 (va_dclの後ろにセミコロンがないことに注意。)

References:
H&S Sec. 11.4 pp. 296-9; CT&P Sec. A.2 pp. 134-139; PCS Sec. 11 pp. 184-5, Sec. 13 p. 250.


15.8:
関数が、実際にいくつ引数を渡されたか知る方法はあるか。

A:

この情報は、移植性の高いプログラムには使えない。古いシステムに は、標準ではないnargs()関数を持つものもあるが、この関数が役に たつかどうかは必ず疑ったほうがいい。この関数は、たいてい引数の 数ではなくワード数を返す(構造体やlongの整数、浮動小数点表示の 数はたいてい、いくつかのワードとして渡される)。

可変個の引数を取る関数は、引数自身から引数の数を判断できるよう に作らなければならない。printfの類は、書式識別子(%dの類)を書式 文字列から捜して引数の数を判断する(だからこの手の関数は書式文 字列が実際の引数と一致していないとおかしな動作をする)。よく使 う別の手で、引数がすべて同じ型のときに便利なのは、番兵(0、-1ま たは適切な型にキャストしたヌルポインター)をリストの最後に置く というものである(質問5.215.4のexeclとvstrcatの例を参照)。最 後に、各引数のデータ型が予測可能なら、可変個の引数の数を明示的 に渡すことができる(ただし、呼び手にしてみれば、いちいち数える のは面倒だろう)。

References:
PCS Sec. 11 pp. 167-8.


15.9:
今使っているコンパイラーは以下のようには関数を宣言させてくれな い。

	int f(...)
	{
	}
すなわち固定長の引数なしでは駄目なようだ。

A:

標準Cは少なくとも1つ固定の引数を必要とする。一つにはva_start() に渡せるように。

References:
ANSI Sec. 3.5.4, Sec. 3.5.4.3, Sec. 4.8.1.1; ISO Sec. 6.5.4, Sec. 6.5.4.3, Sec. 7.8.1.1; H&S Sec. 9.2 p. 263.


15.10:
可変数個引数を取る関数があって、引数としてfloatを取る。なぜ、

	va_arg(argp, float)

ではうまくいかないのか。

A:

可変長の引数並びの可変長の部分には、古い「省略時の引数格上げ」 が適応される。つまりfloatの引数は必ずdoubleに格上げされ(幅を広 げられ)、charとshort intの引数はintに格上げされる。よって va_arg(argp, float);と書くのは常に誤りである。かわりに va_arg(argp, double)と常に書かなければならない。同じように、も ともとはcharやshortやintであった引数を取り出すのには va_arg(argp, int)と呼ばなければならない。質問11.315.2も参照 のこと。

References:
ANSI Sec. 3.3.2.2; ISO Sec. 6.3.2.2; Rationale Sec. 4.8.1.2; H&S Sec. 11.4 p. 297.


15.11:
va_arg()マクロを使ったが、関数へのポインターのデータ型を持つ引 数を取り出すことができない。

A:

va_argマクロを使うことによるデータ型の書き換えは、関数へのポイ ンターなどの極端に複雑なデータ型を相手にすると困難となる。しか し、関数へのポインター型にtypedefを使えば、すべてうまくいくだ ろう。1.21も参照のこと。

References:
ANSI Sec. 4.8.1.2; ISO Sec. 7.8.1.2; Rationale Sec. 4.8.1.2.


15.12:
可変個の引数を持ち、その引数を別の関数(その関数も可変個の引き 数を取る)に渡す関数をどうやって記述すればよいか。

A:

一般的にはできない。理想的には2番目の関数を書き直してva_listを 引数として取る(vfprintf()と同じように、上の質問15.5参照)ように する。引数を本当に引数として直接渡さなければならないときや、2 番目の関数をva_listを引数として取るように書き換える手が選べな いなら(言い換えれば、この2番目の、呼ばれる側の関数が、va_list ではなくて本当に可変数個の引数を取らなければならないなら)移植 性のある解は存在しない。(この問題はマシン固有のアセンブラ言語 まで持ち出せば解決できるだろう。以下の質問15.13も参照のこと)。


15.13:
実行時に、引数リストをつくって関数を呼び出すことができるか。

A:

動くことが保証される方法も移植性の高い方法もない。興味があるの ならこのリストの編者に聞いてみること。彼はヘンテコリンなアイデ アをいくつか持っている

引数リストのかわりに、汎用のポインター(void *)の配列を渡すこと を考えたほうがいいかもしれない。呼ばれた関数は配列を、main()が argvを処理するのと同じように、一つ一つ処理することができ(すぐ わかるように、これは呼ばれる側の関数全体をこちらで管理している 場合に限られる)。

(質問19.36も参照のこと。)

目次へ戻る