コンピューター:C言語講座:可変引数について


 C言語を組んだことがある人でprintf()関数を知らない方はいないと思いますが、printf()関数って不思議だと思ったことはありませんか?思わない人は今回のテーマである可変引数を知っている方か、自分で作ってみようと思ったことの無い方でしょう。不思議なのは引数の数です。printf()では、最初に書式があり、その書式に応じて引数の数が変わります。このような関数を自分で組めますか?
 この課題は知らなければ決してできない課題です。一般的なC言語の規則として引数の個数は固定ですから。では、printf()関数は一体どの様なインチキをして組まれているのでしょうか?ひょっとしてC言語ではできなくて、アセンブラーか何かで組まれているのでしょうか?

可変引数
 実はC言語には可変引数という仕組みがあってこれはなんとも取って付けたような記述なのですが、普通にC言語で記述できます。今回は課題として、デバッグに便利な関数を作ってみましょう。
 皆さんはプログラムのデバッグはどの様に行なっていますか?デバッガーを使うという方はわりと真面目な方?で、多くの方はfprintf(stderrなどで標準エラー出力あたりに変数の値などを表示してデバッグしているのではないでしょうか?私はデバッガーも使いますし、ループなどで値の変化を見たい時などや、気が短い時はfprintf(stderrをよく使います。この方式の最大の問題点は、デバッグが済んで、消そうと思った時にあまりにあちこちに埋め込みすぎてわからなくなってしまう点と、ファイルに出力を出してみたい時やウインドウに表示させたい時に修正するのが大変な点です。何しろデバッグ用ですから、あまり手間がかかるようでは意味がありませんし、製品としてリリースしてから変なメッセージがでるとエラー表示と勘違いしたユーザーから問い合わせが来てしまいます。また、デバッグ表示だ、とわかるように前後に決まった印を入れたいこともあります。
 ファイルに入れるのはリダイレクトすればできますが、エラー出力になっていると、それだけを入れるのはシェルによってはできないこともあります。また、消すのもdefineで囲っておけば簡単に消せますが、出し先を変えたり、複数に出す場合にはたくさんあると大変です。前後に印を入れるのも毎回書くのは大変です。そんな時の為に今回の課題のデバッグ表示プログラムを使えるように考えてみましょう。
 簡単に思いつく方法としては、デバッグ表示用の関数を作っておいて、そこで出し先を変えたり、複数に出したり、出さなくしたりする方法です。基本的にはこれで良いのですが、できれば、printf()のように書式を指定してそれに応じた引数を与えて送りたいものです。そうでないと、文字列以外の物を表示させたい場合には専用の関数を用意するか、sprintf()などで一度文字列にしてから渡す必要があり、大変です。やはり、使い慣れたprintf()形式で行きたいものです。そこで登場するのが、先程の可変引数です。これが使えればprintf()のように自由な個数の引数が受渡しできます。

 では、早速作ってみましょう。可変引数の関数を使う場合には必ずvarargs.hをインクルードする必要があります。

#include    <stdio.h>
#include    <varargs.h>

int debug_print(va_alist)
va_dcl
{
va_list args;
char  *fmt;
char  buf[256];

    va_start(args);
    fmt=va_arg(args,char *);

    vsprintf(buf,fmt,args);

    va_end(args);

    fprintf(stderr,"DEBUG[ %s ]\n",buf);

    return(0);
}

 全く変な書き方ですが、関数の引数としてva_alistを指定し、引数の型を宣言するところでva_dclと書きます。ここにはセミコロンも付けません。そして、関数内で、va_list型のargs(名前は自由)を宣言します。引数を得る前に必ずva_strart(args)でargsが先頭を指すように初期化します。そして、va_arg()を使う度に順に引数が得られます。va_arg()では取り出す型を指定します。char *と書けば文字のポインターという型で取り出されます。最後はva_end()で可変引数の処理の終りを示します。この例ではvsprintf()を呼び出す為に最初の書式の部分だけ取り出して、後はそのままvsprintf()に渡しています。vsprintf()によって書式通りに展開してもらってbufに格納してもらい、それをDEBUG[]で囲って標準エラー出力に出しています。
 実はこれらvarargs関連の関数らしき物は全てマクロで、コンパイラーにプリプロセッサーだけ処理してもらった結果を見ると、大体以下のようになります。

int debug_print(__builtin_va_alist)
int __builtin_va_alist;
{
va_list args;
char  *fmt;
char  buf[4096];

    args = (char *) &__builtin_va_alist;
    fmt=((char **)__builtin_va_arg_incr((char * *)args))[0];

    vsprintf(buf,fmt,args);

    ;

    fprintf((&_iob[2]),"DEBUG[ %s ]\n",buf);
}

 ただ、処理系が異なっても同じソースが使えるように可変引数を使う場合は必ずvarargsを使うようにすることは大切なことで、特にワークステーションとPCでは引数のスタックに積まれる順序なども異なる場合がほとんどですので、自分で無理矢理スタックを操作して実現しようとはしない方が安全です。以前、C言語が日本に来たばかりのころは情報も少なかったので、コンパイラーの正確を理解しながら言語仕様に無いことをしたものですが、現在ではそういった小細工はしなくてもほとんどのものは組めるだけの情報もありますし、移植性も大事です。

まとめ
 可変引数は上記のような使い方以外にもパラメーターの個数が不定の関数を作る時には大変便利で、例えば、引数で与えた複数の値の平均値を得るような場合でも個数に応じて別の関数を作る必要はなく、終了条件を決めて適当に複数個渡して処理させることができます。XツールキットでもXtVaSetValues()のように可変個の引数をNULLでターミネートさせて渡したり、OpenLookでもxv_set()などで同様に記述でき、一旦配列に格納してから渡すよりも簡単に記述できるように配慮されています。私も以前は知らなくて、一体どうやって作るのか悩んだこともあったのですが、知ってしまえば、使うのは簡単です。ぜひ、皆さんのテクニックの1つとして頭の片隅にでも入れておいて使いやすい関数を考えてみてください。


よろしければブログもご覧ください
ipv400153261 from 1998/3/4