printfデバッグTips

作成:

プログラミングを行う上で、printfを使った標準出力は 一番最初の「Hello World」から登場し、 本当にずっとお世話になるものだ、 デバッグをする上でデバッガが使えるようになっても、 完全にprintfなしでとは行かないかもしれない。

というわけで、 このprintfを使ったデバッグ出力を行う上でのTipsについて、 いろいろと解説してみようと思う。

標準出力と標準エラー出力

では、早速だが、printfの他に、 ファイルへの出力を行うときに利用するfprintfという関数がある。 最初期ではないにせよ、プログラミングの勉強のかなり始めうちに使うことになるはずだ。

manを使って定義を確認すると以下のようになっている。

#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);

printffprintfの違いは、 fprintfの最初の引数FILE *streamの有無だ。 なぜ唐突にこんな話を始めたかといえば、printfの出力先って何なんだろう。 ということを説明したいがためだ。

いきなり答えを言ってしまえば、 実は、printffprintfの 最初の引数にstdoutと指定したものと等価である。 (stdoutとはなんぞやと言うのは後ほど)

証拠としては、glibc のソースコードでも眺めてみよう。以下はprintfの実装

int
__printf (const char *format, ...)
{
  va_list arg;
  int done;

  va_start (arg, format);
  done = vfprintf (stdout, format, arg);
  va_end (arg);

  return done;
}

次が、fprintfの実装

int
__fprintf (FILE *stream, const char *format, ...)
{
  va_list arg;
  int done;

  va_start (arg, format);
  done = vfprintf (stream, format, arg);
  va_end (arg);

  return done;
}

fprintfの最初の引数にstdoutを指定すると、 printfと同じになる、と理解いただけたかと思う。

では次に、stdoutとはなにか、となるが、 これはコンソールなどの標準出力を表すFILE型構造体へのポインタ(ファイルストリーム)である。 つまりコンソールへの出力と言うのはプログラムから見たらファイルへの出力と全く同じことをやっている。 ただし、その出力を受けた先が、ストレージに書きだすのではなく、画面に表示するようになっているのだ。 リダイレクトを使う場合、このstdoutが指し示す先が、リダイレクトで指定されたファイルに差し替わって実行される。 全く同じ処理でファイル出力ができてしまう仕組みはこうなっている。

実は、各プロセスごとに暗黙のうちに3つのファイルストリームが自動で用意されている。 (デーモン等の特殊なプロセスは除く) シンボルの宣言は stdio.h の中で行われている。が、仮に stdio.h をincludeしなくても、 これらを直接参照できないだけで、オープンされていることに変わりはない。 それ故、仮にコード上で入出力を一切行わない場合であっても、3つのファイルディスクリプタが消費される。

用意されているのは、標準出力stdout、 標準エラー出力stderr、標準入力stdinである。 標準入力ってのはscanfなどで、コンソールから入力に使われるものである。 今回は関係ないのでこれについては省略する。

標準出力と、標準エラー出力はどちらも明示しなければコンソール上への出力するようにオープンされている。 当然、リダイレクトされている場合はそのリダイレクト先を示す。 標準エラー出力は名前の通り、エラー情報を出力することを前提に使われるもの。 標準出力とファイルディスクリプタが分かれているため、 コンソールへの出力をファイルなどへリダイレクトする際などで、 エラー出力と標準出力を分けて指定できるようになっている。

#include <stdio.h>

int main(int argc, char**argv) {
  fprintf(stdout, "stdout\n");
  fprintf(stderr, "stderr\n");
}

これを実行すると以下のようになり

$ gcc printf.c
$ ./a.out
stdout
stderr

それぞれで、リダイレクトして出力先を分けることができる。

$ ./a.out 1>out.txt 2>err.txt
$ cat out.txt
stdout
$ cat err.txt
stderr

ファイルディスクリプタの値は、 標準出力:1、標準エラー出力:2、標準入力:0、 となっているので明示するとこのような書き方になる。 単に>の記号だけだと、 標準出力のみのリダイレクトとなり、標準エラー出力はコンソールに出力される。 何か処理をした結果を通常はコンソールに出力し、必要に応じてファイルに残すためにリダイレクトする。 という用途を考えた場合、実行中に発生したエラーを除いた、 処理結果のみをファイルに落とせるようになっている。

用途以外の違いはというと、 標準出力は通常、出力バッファを持っており、 コード上で出力を行ったタイミングで表示されるとは限らず、遅延が発生する場合がある。 一方、標準エラー出力では通常、出力バッファを持っておらず、 コード上で出力を行うと直ちに表示される。

バッファの有無は、どちらがいいということではない。 バッファがないと、直ちに表示されるが、 逆に表示が終わるまでプログラム内の出力処理が終わらず、 プログラムの実行が待たされてしまう可能性がある。 バッファがあると、表示されるまでに遅延する可能性があるが、 表示という遅い処理を待たずにバッファに書き出してしまえば、 次の処理ができるのでパフォーマンス上有利となる。 そんなわけで、用途に応じて使い分けるようにしよう。

各ファイルストリームのバッファは以下の関数を使って設定することができる。 stdoutも、この関数でバッファNULL指定すると、バッファなしの動作になる。 逆にもっと多くのバッファが必要な場合は確保したメモリへのポインタを渡すこともできる。 通常は標準で用意されたバッファを変更する必要は無いだろうが、 チューニングを行うにあたって使うこともあるだろう。 安易に使うべきではないが、豆知識として知っておいて損はないかもしれない。

#include <stdio.h>
void setbuf(FILE *stream, char *buf);

また、通常はバッファ付きの出力を行うのだが、 特定の段階で、全部出力させていしまいたいというシーンで使える命令がある。 fflashという命令で、 この引数にstdoutを指定すれば標準出力のバッファを一度掃き出させることができる。

#include <stdio.h>
int fflush(FILE *stream);

printfデバッグで使うべき出力

だいぶ脱線したような気がするが、単にコンソール出力と考えた場合、2つの出力経路があるわけだ。 そのうち、デバッグで使うべきなのは、というと、当然標準エラー出力である。 出力バッファがなく、即表示されるし、通常の実行結果の出力ではないからだ。

ということは、printfデバッグと言いつつも、実は使うべきは標準エラー出力なので、 printf関数ではなく、fprintf関数などを使うことになる。