C言語でのデバッグプリントの効率的なやり方を説明します。


デバッグするときはデバッガを使うのが定石ですが、なんらかの事由によりそれがかなわないときもあるでしょう。 そのとき頼りになるのが片っ端からprintf 文をいれていく方法、いわゆるプリントデバッグです。 CPUのレジスタなどはわかりませんが、単にどこまでプログラムが実行されたか知りたいだけ、とかならこれでもなんとかなります。2分探索で切り分けていけば、仮に100万行のコードでも高々20回の試行ですみます。 今回はこのデバッグプリントのTipsをご紹介します。

デバッグビルドとリリースビルドとで有効無効を切り替える

printfデバッグするときには、リリース時にはそのデバッグプリント文は削除する必要があります。 手作業でそれをやっていては削除漏れが起きてしまう可能性大です。そこで、プリプロセッサの条件コンパイルをつかってそれを自動化しましょう。

例としては下記のようになります。この例では、マクロDEBUG_BUILDが定義されていればデバッグプリントが有効に、定義されていなければ無効になります。 コードの内容の説明はあとでします。ひとまずここでは、有効無効の切り替え方をみていきます。

1
2
3
4
5
6
7
#ifdef DEBUG_BUILD
# define DEBUG_PUTS(str) puts(str)
# define DEBUG_PRINTF(fmt, ...)  printf(fmt, __VA_ARGS__);
#else
# define DEBUG_PUTS(str)
# define DEBUG_PRINTF(fmt, ...)
#endif

ヘッダファイル内のマクロ定義でデバッグプリント有効無効を切り替える

このデバッグプリントの有効無効の切り替え方ですが、一番シンプルなのはマクロDEBUG_BUILDをヘッダファイル内で定義してしまうことです。 例えば下記のようなヘッダファイルを作って、それをインクルードするようにすればOKです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#ifndef MY_DEBUG_H_INCLUDED
#define MY_DEBUG_H_INCLUDED

#define DEBUG_BUILD  // enable debug print.

#ifdef DEBUG_BUILD
# define DEBUG_PUTS(str) puts(str)
# define DEBUG_PRINTF(fmt, ...)  printf(fmt, __VA_ARGS__);
#else
# define DEBUG_PUTS(str)
# define DEBUG_PRINTF(fmt, ...)
#endif

#endif

無効にしたい場合は下の行をコメントアウトすればOKです。

1
#define DEBUG_BUILD  // enable debug print.

このやり方はシンプルなのでわかりやすいのですが、いちいちヘッダファイルを書き換えて#define DEBUG_BUILDを有効にしたりコメントアウトしたり、という手間があります。

コンパイルオプションでデバッグプリント有効無効を切り替える

一方、ヘッダファイルを書き換えなくてもよい方法があります。 ヘッダファイルには、下記のようにDEBUG_BUILDの定義は書きません。 その代わり、コンパイルオプションを利用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#ifndef MY_DEBUG_H_INCLUDED
#define MY_DEBUG_H_INCLUDED

#ifdef DEBUG_BUILD
# define DEBUG_PUTS(str) puts(str)
# define DEBUG_PRINTF(fmt, ...)  printf(fmt, __VA_ARGS__);
#else
# define DEBUG_PUTS(str)
# define DEBUG_PRINTF(fmt, ...)
#endif

#endif

GCCやたいていのコンパイラには-Dオプションがあります。 これはコンパイル時にマクロを定義するためのオプションです。たとえば、

1
gcc hoge.c -DFUGA -c -o hoge.o

とすれば、これはhoge.cの先頭に #define FUGAと書いたときと同じようにコンパイルされます。 これを利用して、

1
gcc hoge.c -DDEBUG_BUILD -c -o hoge.o

とすれば、そのファイルは先頭に #define DEBUG_BUILDと書いたときとおなじになり、DEBUG_BUILDが有効になります。 Makefileを利用しているなら、CFLAGS変数にこのオプションを追加すればOKでしょう。 ただし、たいていのMakefileの書き方では、コンパイルオプションが変わっても自動ではリビルドしてくれないので、 明示的にmake cleanをするのを忘れずに。

Note

GNU Makefile のフレームワークを GitHub のgnu-make-framework-zenに置いています。 ご参考までに。

可変長引数とマクロ関数

簡単にコードの説明をします。 まず、フォーマット引数の必要がない場合、つまり%d%sなどがない場合はputsで事足りるのでそれを使います。 putsは引数は1つですので普通のマクロ関数で定義すればOKです。

フォーマット引数が必要な場合はprintfを使います。printfは可変長引数をとるので、関数マクロも可変長引数をとれるようにします。 関数マクロDEBUG_PRINTFの引数の ...はC99からの新機能で、可変長引数を表します。その可変長引数に対応するのが__VA_ARGS__です。 たとえば

1
DEBUG_PRINTF("%s, %d.\n", "hoge", 3);

としたとき、可変長引数には"hoge", 3が対応し、

1
printf("%s, %d.\n", "hoge", 3);

と展開されます。ちなみにfmtには最初の引数"%s, %d.\n"が対応します。 注意点として、この例ではDEBUG_PRINTFDEBUG_PUTSを使い分けなくてはいけません。 フォーマット引数のない場合はDEBUG_PRINTFはつかえません。 可変長引数の...および__VA_ARGS__に対応するものがないためです。たとえば

1
DEBUG_PRINTF("hello.\n");

とした場合、

1
printf("hello.\n",);

と展開されてしまい、よけいなカンマが残ってしまいます。

gcc拡張を使ってもっと便利に

上記の例では、printfへのフォーマット引数の有無でDEBUG_PUTSDEBUG_PRINTFを使い分ける必要がありました。 それはめんどくさいのですが、gcc拡張がつかえるコンパイラなら、下記のように書くことができます。文字連結のマクロ ##を追加します。

1
2
3
4
5
#ifdef DEBUG_BUILD
# define DEBUG_PRINTF(fmt, ...)  printf(fmt, ## __VA_ARGS__);
#else
# define DEBUG_PRINTF(fmt, ...)
#endif

これによりDEBUG_PRINTF("hello.\n");のようにフォーマット引数がない場合でもDEBUG_PRINTFを使えるようになります。 今回利用しているgcc拡張についての詳細は、 3.6 Variadic Macrosのサイトを見てみてください。

自動でファイル名、関数名、行数も表示する

どこまでプログラムが進んだかを知りたいときには、ファイル名や行数を表示させると便利です。 事前定義済みマクロ(プリでファインドマクロ)を使えばそれが簡単に可能です。 ファイル名は__FILE__ 行数は__LINE__で取得できます。関数名はC99からの新機能で__func__で取得可能です。 関数名のマクロは小文字なので注意してください。

これらを使った例を下に示します。このようなヘッダファイル my_debug.h を作ったとします。

1
2
3
4
5
6
7
8
9
#define DEBUG_BUILD

#ifdef DEBUG_BUILD
# define DEBUG_PRINTF(fmt, ...) \
     printf("file : %s, line : %d, func : %s, " fmt, \
                      __FILE__, __LINE__, __func__, ## __VA_ARGS__);
#else
# define DEBUG_PRINTF(fmt, ...)
#endif

それを下記のようにコールしたとします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include "my_debug.h"

int main(void)
{
    DEBUG_PRINTF("hoge.\n");

    DEBUG_PRINTF("fuga.\n");

    DEBUG_PRINTF("pino.\n");

    return 0;
}

実行結果は下記のようになります。このように自動的にファイル名や関数名、行数が表示されます。

1
2
3
file : main.c, line : 5, func : main, hoge.
file : main.c, line : 7, func : main, fuga.
file : main.c, line : 9, func : main, pino.

もしくは do-while をつかってprintf2つで書くことも可能です。 実行結果は同じなります。お好みでお好きな方をどうぞ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#ifndef MY_DEBUG_H_INCLUDED
#define MY_DEBUG_H_INCLUDED

#define DEBUG_BUILD

#ifdef DEBUG_BUILD
# define DEBUG_PRINTF(fmt, ...)                             \
            do {                                            \
                printf("file : %s, line : %d, func : %s, ", \
                       __FILE__, __LINE__, __func__);       \
                printf(fmt, ##__VA_ARGS__);                 \
            } while (0)
#else
# define DEBUG_PRINTF(fmt, ...)
#endif

#endif

補足

ここまでくれば、割と便利にプリントデバッグできるかなと思います。念のため、さらに補足をしておきます。

マクロ関数をdo { } while(0) で囲む理由

下記のコードを見て「なぜマクロ関数をdo while(0)で囲んでいるんだろう、無駄なのでは」と思った方もいるかもしれません。 これはマクロ関数を使うときにイディオムです。わりとよく使われます。

1
2
3
4
5
6
# define DEBUG_PRINTF(fmt, ...)                             \
            do {                                            \
                printf("file : %s, line : %d, func : %s, ", \
                       __FILE__, __LINE__, __func__);       \
                printf(fmt, ##__VA_ARGS__);                 \
            } while (0)

わかりやすいように、次の2つのマクロ関数を考えてみます。処理内容はテキトウです。 do while があるかどうかに着目してください。

1
2
#define MACRO_FUNC1(...)    { printf("MACRO_FUNC1 "); printf(__VA_ARGS__);}
#define MACRO_FUNC2(...) do { printf("MACRO_FUNC2 "); printf(__VA_ARGS__);} while(0)

この2つの関数を下記のようにコールしたとします。

1
2
3
4
5
6
7
8
9
void func(int tmp)
{
    if (tmp)
        MACRO_FUNC1("hoge\n");
    else
        MACRO_FUNC2("fuga\n");

    return;
}

すると下記のように展開されます。まずい点がありますね。

1
2
3
4
5
6
7
8
9
void func(int tmp)
{
    if (tmp)
        { printf("MACRO_FUNC1 "); printf(hoge);};
    else
        do { printf("MACRO_FUNC2 "); printf(fugue);} while(0);

    return;
}

ダメな点がわかりやすいように改行とインデントを入れてみます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void func(int tmp)
{
    if (tmp) {
        printf("MACRO_FUNC1 ");
        printf(hoge);
    } ;
    else
        do {
            printf("MACRO_FUNC2 ");
            printf(fugue);
        } while(0);

    return;
}

そうですね、if節の終わりの}の後に余計なセミコロンがあります。 これは邪魔です、コンパイルエラーになります。do-while(0)の方は、セミコロンまでが一つの文なので問題ありません。 仮にif節の方にdo-while(0)のマクロ関数を持ってきてもコンパイルエラーにはなりません。 このようにぶら下がりif文で予期しないエラーを出さないためのイディオムがdo-while(0)なわけですね。

printfのバッファリング

実は下記の書き方、途中でセグメンテーションフォルトなどで異常終了する場合、期待通りに動かないかもしれません。 というのは、printfは一時的に出力をバッファリングして(溜めて)から、一気に出力するからです。

1
2
3
4
5
6
# define DEBUG_PRINTF(fmt, ...)                             \
            do {                                            \
                printf("file : %s, line : %d, func : %s, ", \
                       __FILE__, __LINE__, __func__);       \
                printf(fmt, ##__VA_ARGS__);                 \
            } while (0)

標準関数のsetvbufでバッファリングの挙動を変えることは可能ですが、素直にfprintfで標準エラー出力に吐き出した方が賢いかもしれません。fprintfを使った場合は下記のようになりますね。

1
2
3
4
5
6
# define DEBUG_PRINTF(fmt, ...)                                      \
            do {                                                     \
                fprintf(stderr, "file : %s, line : %d, func : %s, ", \
                       __FILE__, __LINE__, __func__);                \
                fprintf(stderr, fmt, ##__VA_ARGS__);                 \
            } while (0)

まとめ

今回は、現場で案外使われる手法デバッグプリントについて説明しました。 ついでに条件コンパイルやdo-while(0)イディオムなども説明しました。ラクしてデバッグできるようになるといいなと思います!