C言語でのデバッグプリントの効率的なやり方を説明します。
デバッグするときはデバッガを使うのが定石ですが、なんらかの事由によりそれがかなわないときもあるでしょう。 そのとき頼りになるのが片っ端からprintf 文をいれていく方法、いわゆるプリントデバッグです。 CPUのレジスタなどはわかりませんが、単にどこまでプログラムが実行されたか知りたいだけ、とかならこれでもなんとかなります。2分探索で切り分けていけば、仮に100万行のコードでも高々20回の試行ですみます。 今回はこのデバッグプリントのTipsをご紹介します。
デバッグビルドとリリースビルドとで有効無効を切り替える
printfデバッグするときには、リリース時にはそのデバッグプリント文は削除する必要があります。
手作業でそれをやっていては削除漏れが起きてしまう可能性大です。そこで、プリプロセッサの条件コンパイルをつかってそれを自動化しましょう。
例としては下記のようになります。この例では、マクロDEBUG_BUILDが定義されていればデバッグプリントが有効に、定義されていなければ無効になります。
コードの内容の説明はあとでします。ひとまずここでは、有効無効の切り替え方をみていきます。
| |
ヘッダファイル内のマクロ定義でデバッグプリント有効無効を切り替える
このデバッグプリントの有効無効の切り替え方ですが、一番シンプルなのはマクロDEBUG_BUILDをヘッダファイル内で定義してしまうことです。
例えば下記のようなヘッダファイルを作って、それをインクルードするようにすればOKです。
| |
無効にしたい場合は下の行をコメントアウトすればOKです。
| |
このやり方はシンプルなのでわかりやすいのですが、いちいちヘッダファイルを書き換えて#define DEBUG_BUILDを有効にしたりコメントアウトしたり、という手間があります。
コンパイルオプションでデバッグプリント有効無効を切り替える
一方、ヘッダファイルを書き換えなくてもよい方法があります。
ヘッダファイルには、下記のようにDEBUG_BUILDの定義は書きません。
その代わり、コンパイルオプションを利用します。
| |
GCCやたいていのコンパイラには-Dオプションがあります。
これはコンパイル時にマクロを定義するためのオプションです。たとえば、
| |
とすれば、これはhoge.cの先頭に #define FUGAと書いたときと同じようにコンパイルされます。
これを利用して、
| |
とすれば、そのファイルは先頭に #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__です。
たとえば
| |
としたとき、可変長引数には"hoge", 3が対応し、
| |
と展開されます。ちなみにfmtには最初の引数"%s, %d.\n"が対応します。
注意点として、この例ではDEBUG_PRINTFとDEBUG_PUTSを使い分けなくてはいけません。
フォーマット引数のない場合はDEBUG_PRINTFはつかえません。
可変長引数の...および__VA_ARGS__に対応するものがないためです。たとえば
| |
とした場合、
| |
と展開されてしまい、よけいなカンマが残ってしまいます。
gcc拡張を使ってもっと便利に
上記の例では、printfへのフォーマット引数の有無でDEBUG_PUTSとDEBUG_PRINTFを使い分ける必要がありました。
それはめんどくさいのですが、gcc拡張がつかえるコンパイラなら、下記のように書くことができます。文字連結のマクロ ##を追加します。
| |
これによりDEBUG_PRINTF("hello.\n");のようにフォーマット引数がない場合でもDEBUG_PRINTFを使えるようになります。
今回利用しているgcc拡張についての詳細は、
3.6 Variadic Macrosのサイトを見てみてください。
自動でファイル名、関数名、行数も表示する
どこまでプログラムが進んだかを知りたいときには、ファイル名や行数を表示させると便利です。
事前定義済みマクロ(プリでファインドマクロ)を使えばそれが簡単に可能です。
ファイル名は__FILE__ 行数は__LINE__で取得できます。関数名はC99からの新機能で__func__で取得可能です。
関数名のマクロは小文字なので注意してください。
これらを使った例を下に示します。このようなヘッダファイル
my_debug.h を作ったとします。
| |
それを下記のようにコールしたとします。
| |
実行結果は下記のようになります。このように自動的にファイル名や関数名、行数が表示されます。
| |
もしくは do-while をつかってprintf2つで書くことも可能です。
実行結果は同じなります。お好みでお好きな方をどうぞ。
| |
補足
ここまでくれば、割と便利にプリントデバッグできるかなと思います。念のため、さらに補足をしておきます。
マクロ関数をdo { } while(0) で囲む理由
下記のコードを見て「なぜマクロ関数をdo while(0)で囲んでいるんだろう、無駄なのでは」と思った方もいるかもしれません。
これはマクロ関数を使うときにイディオムです。わりとよく使われます。
| |
わかりやすいように、次の2つのマクロ関数を考えてみます。処理内容はテキトウです。
do while があるかどうかに着目してください。
| |
この2つの関数を下記のようにコールしたとします。
| |
すると下記のように展開されます。まずい点がありますね。
| |
ダメな点がわかりやすいように改行とインデントを入れてみます。
| |
そうですね、if節の終わりの}の後に余計なセミコロンがあります。
これは邪魔です、コンパイルエラーになります。do-while(0)の方は、セミコロンまでが一つの文なので問題ありません。
仮にif節の方にdo-while(0)のマクロ関数を持ってきてもコンパイルエラーにはなりません。
このようにぶら下がりif文で予期しないエラーを出さないためのイディオムがdo-while(0)なわけですね。
printfのバッファリング
実は下記の書き方、途中でセグメンテーションフォルトなどで異常終了する場合、期待通りに動かないかもしれません。
というのは、printfは一時的に出力をバッファリングして(溜めて)から、一気に出力するからです。
| |
標準関数のsetvbufでバッファリングの挙動を変えることは可能ですが、素直にfprintfで標準エラー出力に吐き出した方が賢いかもしれません。fprintfを使った場合は下記のようになりますね。
| |
まとめ
今回は、現場で案外使われる手法デバッグプリントについて説明しました。
ついでに条件コンパイルやdo-while(0)イディオムなども説明しました。ラクしてデバッグできるようになるといいなと思います!