このページの解説は C99 をベースとしています。
以下は目次です。
これまで関数を自作する際、仮引数には 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)
{
("ddcd", 10, 20, 'x', 30);
print("ss", "abc", "def");
print("dfc", 50, 3.3f, 'Z');
print}
/*
標準出力へ任意の個数・型の値を出力する
引数:
format: 出力フォーマットを表す文字を並べたもの。
d … 符号付き整数型
f … 実浮動小数点型
c … 文字型
s … 文字列型
とする。
たとえば、"dds" と指定すると、
後続の実引数が 整数型, 整数型, 文字列型 の順番で並んでいるものと判断される。
...: 出力する値のリスト
*/
void print(const char* format, ...)
{
va_list args;
(args, format);
va_start
for (const char* p = format; *p != '\0'; ++p) {
switch (*p) {
case 'd':
("%d ", va_arg(args, int));
printfbreak;
case 'f':
("%lf ", va_arg(args, double));
printfbreak;
case 'c':
("%c ", va_arg(args, char));
printfbreak;
case 's':
("%s ", va_arg(args, const char*));
printfbreak;
default:
(!"不正な変換指定");
assertbreak;
}
}
("\n");
printf
(args);
va_end}
実行結果:
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つ取り出し、その値をキャスト📘して、標準出力へ出力します。
正確にいえばC言語には文字列型という型はありません。文字型の配列のことを指しています。
この過程の中で、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関数で double型の値を扱うときの変換指定が float型と同じ “%f” で構わないのに対し(“%lf” でもいいです)、scanf関数では “%lf” としなければならない(第20章)理由はここにあります。
printf関数で実浮動小数点型を扱う場合、実引数に float型と double型のどちらの値を渡すとしても、結局は暗黙的に double型に変換されるため、float と double を区別する意味がありません。そのためどちらの型の場合でも “%f” で扱えます。
long double型の場合は “%Lf” を使わないといけません。これは、暗黙的な型の拡張と関わっていないので、区別を付けなければならないためです。
一方、scanf関数で実浮動小数点型を扱う場合、実引数に指定するものは、float型や double型のポインタ型です。これは実浮動小数点型ではなくポインタ型なので、暗黙的な型の拡張とは無関係です。ポインタが指し示す先へ結果を代入するため、間違った大きさの値を代入しないように、変換指定を使い分けて、型の区別を付けなければなりません。
こちらも、long double型の場合は “%Lf” を使わないといけません。
今度は、自作のログ出力関数を作ってみましょう。printf関数と同じ形式で引数を渡すと、その内容を標準出力と、テキストファイルに同時に書き出すものとします。要するに、次の2つの文を1つにまとめた関数を作ります。
("value0: %d value1: %d\n", value0, value1);
printf(fp, "value0: %d value1: %d\n", value0, value1); fprintf
まず、可変個の引数に対応できないといけないことはいうまでもありません。とりあえず、思いつくままに書いてみます。
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
void output_log(FILE* fp, const char* str, ...);
int main(void)
{
FILE* fp = fopen("log.txt", "a");
if (fp == NULL) {
("ファイルオープンに失敗しました。\n", stderr);
fputs(EXIT_FAILURE);
exit}
int value0 = -100;
int value1 = 100;
(fp, "test message\n");
output_log(fp, "value0: %d value1: %d\n", value0, value1);
output_log
if (fclose(fp) == EOF) {
("ファイルクローズに失敗しました。\n", stderr);
fputs(EXIT_FAILURE);
exit}
}
/*
標準出力と、任意のファイルへ出力
引数:
fp: 出力先ファイルのポインタ。
str: 出力するメッセージ。
出力形式は、printf関数と同様。
引数fp の指定に関わらず、標準出力へは出力される。
引数fp がヌルポインタの場合は、標準出力にのみ出力する。
*/
void output_log(FILE* fp, const char* str, ...)
{
va_list args;
(args, str);
va_start
(str, args);
printf
if (fp != NULL) {
(fp, str, args);
fprintf}
(args);
va_end}
実行結果:
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* restrict format, va_list args);
int vfprintf(FILE* restrict fp, const char* restrict format, va_list args);
restrict については、第57章で取り上げます。動作に影響はないので、今は無視して問題ありません。
printf関数や、fprintf関数が仮引数に … を持っているのに対し、vprintf関数や vfprintf関数は va_list型の仮引数を持ちます。ですから、va_list型の変数を渡せます。これらの関数に置き換えてみましょう。
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
void output_log(FILE* fp, const char* str, ...);
int main(void)
{
FILE* fp = fopen("log.txt", "a");
if (fp == NULL) {
("ファイルオープンに失敗しました。\n", stderr);
fputs(EXIT_FAILURE);
exit}
int value0 = -100;
int value1 = 100;
(fp, "test message\n");
output_log(fp, "value0: %d value1: %d\n", value0, value1);
output_log
if (fclose(fp) == EOF) {
("ファイルクローズに失敗しました。\n", stderr);
fputs(EXIT_FAILURE);
exit}
}
/*
標準出力と、任意のファイルへ出力
引数:
fp: 出力先ファイルのポインタ。
str: 出力するメッセージ。
出力形式は、printf関数と同様。
引数fp の指定に関わらず、標準出力へは出力される。
引数fp がヌルポインタの場合は、標準出力にのみ出力する。
*/
void output_log(FILE* fp, const char* str, ...)
{
va_list args;
(args, str);
va_start(str, args);
vprintf(args);
va_end
if (fp != NULL) {
(args, str);
va_start(fp, str, args);
vfprintf(args);
va_end}
}
実行結果(標準出力):
test message
value0: -100 value1: 100
実行結果(log.txt):
test message
value0: -100 value1: 100
今度は正しい結果を得られています。
ここで、vprintf関数や vfprintf関数を呼び出すたびに、va_startマクロと va_endマクロで囲んでいますが、これを次のように1つにしてしまうと、正しく動作しない可能性があります。
void output_log(FILE* fp, const char* str, ...)
{
va_list args;
(args, str);
va_start
(str, args);
vprintf
if (fp != NULL) {
(fp, str, args); // 正しく動作しないかもしれない
vfprintf}
(args);
va_end}
これは、規格上、vprintf関数や vfprintf関数といった va_list型の仮引数を持った標準ライブラリ関数を呼んだ後、渡した va_list型の変数の内容がどうなっているかを保証していないからです。ですから、これらの関数を複数呼び出す場合には、その都度、va_startマクロと va_endマクロで囲むようにして、毎回、可変個の引数の処理をやり直させる必要があります。
あるいは、va_copyマクロを使う方法があります。
void output_log(FILE* fp, const char* str, ...)
{
va_list args, args2;
(args, str);
va_start(args2, args);
va_copy
(str, args);
vprintf
if (fp != NULL) {
(fp, str, args2);
vfprintf}
(args2);
va_end(args);
va_end}
va_copyマクロは、第2引数の va_list の内容を、第1引数の va_list へコピーします。
va_copy を使えば、2つ(あるいはそれ以上)の va_list を独立したものとして扱えますから、そのつど、va_startマクロと va_endマクロで囲むような対策が不要になります。
なお、va_copy で作られたコピーのほうに対しても va_end を適用する必要があるので、忘れないようにしてください。
va_copy は C99規格で追加されました。
最後に、printf関数、scanf関数系の関数を整理しておきます。
char型バージョン |
wchar_t型バージョン |
備考 |
||
---|---|---|---|---|
… を使う |
va_list型 を使う |
… を使う |
va_list型 を使う |
|
標準出力へ出力 |
||||
任意のストリームへ出力 |
||||
文字の配列へ出力。swprintf、vswprintf はバッファ長の指定が加わっている。 |
||||
文字の配列へ出力。バッファ長指定版。wchar_t型版の名前に n は含まれない。 |
||||
標準入力から入力 |
||||
任意のストリームから入力 |
||||
文字列から入力 |
かなり複雑です。また、似た名前で非標準の関数が用意されている環境もあるため、混乱に拍車がかかっています。記憶する必要はないので、使うときに調べればいいのですが、複雑さに対する覚悟が必要かもしれません。
関数形式マクロの引数も可変個にできます。
#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;
("%d\n", n);
PRINT("%s %d\n", s, n);
PRINT}
実行結果:
123
abc 123
関数形式マクロを定義するとき、引数が可変個である部分を ...
とします。関数で可変個引数を使う場合と違って、...
は1個以上の引数を表しており、...
の手前に他の引数がなくても構いません。
...
の部分を置換結果の中で使用するには、__VA_ARGS__ を使用します。__VA_ARGS__ は、可変個引数の部分に指定された実引数の内容によって(引数の区切りのコンマも含めて)置換されます。
たとえば以下の文は、
("%d\n", n); PRINT
以下のように置換されます。
(stderr, "%d\n", n); fprintf
可変個引数の部分に指定する実引数が 0個でも構わないせいで、置換結果をうまく表現できないことがあります。たとえば #define M(...) f(0, __VA_ARGS__)
に対して、M(10, 20);
は f(0, 10, 20);
に置換できますが、M();
は f(0, );
になってしまい ,
が余分になります(Visual Studio ではこれでもコンパイルが通ってしまいますが)。
【C23】 __VA_OPT__ が追加されました[1]。可変個引数の部分が 1個以上指定されたときにだけ置換をおこなうことを表現できます。この例の場合なら、#define M(...) f(0 __VA_OPT__(,) __VA_ARGS__)
としておけば、可変個引数が 0個のときには置換結果に ,
が現れなくなり、f(0);
に置換できます。
問題① 可変個引数で渡した int型整数の合計値を返す関数を作成してください。可変でない1個目の引数が、可変個部分の引数の個数を表すとします。たとえば、
= sum(5, 10, -4, 7, -2, 9); total
このように呼び出すと、変数total に 10 + (-4) + 7 + (-2) + 9 の結果である 20 が格納されるものとします。
問題② %d、%f、%c、%s の各変換指定子にだけ対応した、簡易的な printf関数を自作してください。“%3d” などの複雑な仕様は無視して構いません。また、実際に標準出力へ書き出す部分は、本物の printf関数を呼び出して構いませんが、vprintf関数は使わないでください。
問題③ 配列へ要素をまとめて格納する関数を作成してください。たとえば、
(array, 5, 0, 1, 2, 3, 4); assign
このように呼び出すと、int型で要素数が 5 の配列array に、0, 1, 2, 3, 4 という値を順番に格納するものとします。
()
の前後の空白の空け方)(
の直後、)
の直前に空白を入れない)return 0;
を削除(C言語編全体でのコードの統一)
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
![]() |
管理者情報 | プライバシーポリシー |