【C言語】可変個引数(va_start,va_end…)のサンプルプログラム

C言語
本記事で分かること
  • 可変個引数関連の関数、マクロにはどんなものがあるか
  • 具体的な使用例はどのようなものか

製品となるプログラムには、あらかじめトレースログを仕込んでおくことが大変重要です。不具合が発生した後で、デバッグログを追加して調査してもよいのですが、不具合が滅多に再現しない類のもの、例えば、メモリ使用率や負荷等により発生するようなものだと、後からログを追加しても遅いのです。したがって、製品の設計時は、ログ設計も同時に行うべきです。

本投稿では、サンプルプログラムを作成し、C言語の可変個引数を利用したトレース処理を解説します。

実行環境は下表の通りです。

開発環境Microsoft Visual Studio Community 2019 Version 16.11.7
OSWindows 10 バージョン21H1
実行環境
スポンサーリンク
スポンサーリンク

登場する関数

本サンプルでは、以下の関数、およびマクロを用いて、トレース関数を作成します。

関数名/マクロ名 機能/説明
va_list可変個引数オブジェクトのデータ型。
va_start可変個引数の取得を開始する。 [マクロ]
va_end可変個引数の取得を終了する。 [マクロ]
vfprintfファイルに可変長引数を用いた文字列を出力することが可能。
va_arg可変個の中で現在ポインタが指す引数を取り出すことが可能。[マクロ]
可変個引数関連の関数/マクロ群

トレース関数仕様

プログラムにトレースを挿入する際には、プログラム内部の変数を表示する場合と、表示しないケースがあります。

各トレース箇所で、表示したい内部変数の数により、fprintf文で変換フォーマットを作ってしまうと、類似したコードを多数埋め込まなくてはならず、非効率なコードと言えます。例えば、ログの出力パターンの数が決まっていれば、トレース関数内で、switch文でパターン毎に分岐するといった思いつきもあるかもしれませんが、使いづらいのでやめましょう。

今回のトレース関数は下記のインターフェースとします。

【仕様】
関数名:void Trace(int level, char format...)
引数 :level …レベル(0=正常、1=異常)
        format…ログ出力フォーマット

トレースプログラム

vfprintfを使用するケース

サンプルプログラムTrace関数は以下のように実装します。 va_startマクロは、第1引数はva_list型のオブジェクトを設定し、第2引数は関数の引数に渡された最初の省略記号がついている引数を設定することになっています。下記の例では、”format”となります。

void Trace(int level, const char* format ...)
{
	FILE* fp = NULL;
	errno_t err = 0;
	va_list ap;
	va_start(ap, format);
	if (level == 0)
	{
		err = fopen_s(&fp, "C:\\temp\\log.txt", "at");		// ファイルオープン
	}	else {
		err = fopen_s(&fp, "C:\\temp\\error.txt", "at");	// ファイルオープン
	}
	if (err != 0)
	{
		printf("fopen_s error\n");
		exit(1);
	}
	vfprintf(fp, format, ap);							// ログをファイルへ出力
	fclose(fp);											// ファイルクローズ
	va_end(ap);
}

Trace関数の引数levelで、正常ログか異常ログかを切り替えることができます。ファイルパスをchar型の別配列に格納すれば、fopen_sを1行にすることもできますね。(そっちの方がよいです)

main関数では、Trace関数を次のように使用できます。

#define NORMAL 0
#define ABNORMAL 1
void main(void)
{
	int a = 7;
	int b = 3;
	float f = 1.5;
	const char str[] = "xyz";
	Trace(NORMAL, "a=%d b=%d\n", a, b);				// (1)変数2つ(int)
	Trace(NORMAL, "a=%d\n", a);						// (2)変数1つ(int)
	Trace(NORMAL, "TEST\n");						// (3)変数無し(文字列)
	Trace(NORMAL, "float=%f\n", f);					// (4)変数1つ(float)
	Trace(NORMAL, "hoge=[%s]\n", str);				// (5)変数1つ(char)
}

実行結果として出力されるログは以下の通りです。

【実行結果】
a=7 b=3
a=7
TEST
float=1.500000
hoge=[xyz]

va_argを使用するケース

vfprintfを使えばよいのですが、参考までにva_argを使うケースにも触れておきます。サンプルプログラムは以下の通りです。可変個引数を使用した関数では、第1引数はva_list型オブジェクトで取り出すことはできません。

va_argは可変個引数の中から次の引数を取り出すのに使います。(つまり、省略記号”…”がついた変数以降) 取り出した引数のデータ型が分からないので、第1引数でその情報を渡したりすることが多いようです。

void Trace2(const char* format ...)
{
	FILE* fp = NULL;
	errno_t err = 0;
	char *str = NULL;
	va_list ap;
	va_start(ap, format);
	err = fopen_s(&fp, "C:\\temp\\log1.txt", "at");		// ファイルオープン
	if (err != 0)
	{
		printf("fopen_s error\n");
		exit(1);
	}
	// 第1引数は固定のため、va_argでは取得不可("cif"を解析して処理を分岐させたり)
	fprintf(fp, "%s\n", va_arg(ap, char*));
	fprintf(fp, "%d\n", va_arg(ap, int));
	fprintf(fp, "%lf\n", va_arg(ap, double));
	fclose(fp);											// ファイルクローズ
	va_end(ap);
}
void main(void)
{
	Trace2("cif", "num2", 3, 2.9);   // c:char i:int f:double
}
【実行結果】
num2
3
2.900000

まとめ

可変個引数を用いたトレース処理を作る場合は、以下を理解しましょう。

  • 関数宣言において、省略記号”…”を使用する
  • va_list型オブジェクトを宣言し、va_startでオブジェクト初期化、va_endで可変個引数を終了する

ログ出力を行う手段として、vfprintfを使用するかva_argで引数を取り出す処理を行うか、になりますが、vfprintfの方が直感的でよいでしょう。

参考リンク

va_arg, va_copy, va_end, va_start
Learn more about: va_arg, va_copy, va_end, va_start
Functions with Variable Argument Lists (C++)

コメント

タイトルとURLをコピーしました