2014年6月10日火曜日

デバッグ手法について



プログラムが意図したとおりに動かない時は、

バグ(誤り)を発見し修正する「デバッグ」という

作業を行わなければなりません。

ちなみに、サンプルプログラムは

全く意味のないものです(^^;)


エラーの種類
言語を問わず、プログラミングをしていてまず最初に突き当たる壁が

コンパイルエラーです。プログラムがコンパイルできない原因は、

文法が間違っている、コンパイル方法が正しくない、などがありますが、

どの部分が間違っているのかはたいていコンパイラが指摘してくれます。

ただし、コンパイラが出力するエラーメッセージは、良くも悪くも

「流れ作業的」なので、ほんの一箇所間違いがあるだけでも、それ以降で

整合性が取れず、エラーメッセージが多数出力されることがあります。

エラーメッセージがたくさん表示されたとしても、間違いと関係ないものも

ありますので、どのメッセージが本質的なものなのかを見極めることも

必要となります。

また、コンパイルでエラーになっていないからといって、それが正しく

動作するかは別の話です。

一番厄介なのは、プログラム実行中の不具合です。

例えば、プログラムが途中で止まる、あるいは異常終了するとか

動作は期待通りであるが、出力結果がおかしいなど、バグの種類も

様々です。

変数 a に5を代入は 「a = 5;」 と書きます。これを間違って「a == 5;」 と

イコールを二つ書いたとしても、a と 5 が等しいかどうかを比較する式

なので、文法的には正しく、コンパイルエラーにはなりません。

しかし、a に 5 は代入されず不定値のままなので、意図した動作に

ならないプログラムの出来上がりです(-_-;)


バグの発見
プログラムのバグを発見するには、まずソースコードをトレースするのが

基本です。そのロジックが正しいのかを再度確認する必要があります。

しかし、何度も見直したけどおかしいところはない、なぜ期待した結果と

異なるのか、と意外と誤りに気付かないケースもあります。

すぐに誰かに聞くのも一つの手ですが、まずは自分で解決するための

方法を押さえておきたいところです。


・ 処理を分割する
C++では、式や文の記述が非常に柔軟なので、かなり凝ったプログラムも

作ることができます。ただ、そういったものは他人には分かりにくく保守性

が悪い、いわゆる自己満足にしかならないプログラムとなる場合が多いです。

バグの温床にもなりますので、処理や意味の単位で分割することを

考えた方がよいでしょう。短く書こうとして、あまり一行にまとめすぎると

どこでエラーが起きているかも分かりにくくなります。

演算子の優先順位も間違いやすいところです。式の意味が分かりにくいとか

優先順位がはっきりしない、などの場合は、カッコをつけたり、一度変数に

代入すると読みやすくなるものです。

ポインタや配列の演算、インクリメント演算子などを多用すると

複雑になりますので、特に注意してください。

「困難は分割せよ」です(^^)


・ print文を挿入する
コンパイルしたプログラムを実行しただけでは、バグがあることは分かっても

原因の特定は難しいでしょう。このような時は、トレーサとして cout あるいは

printf を挿入して手がかりを掴めるようにします。例えば、プログラム中で

処理を分岐しているところに、cout << "実行1" << endl; 、cout << "実行2" << endl;

などと仕込んでおけば、そこに到達した時にメッセージが出力されますので

どの条件でこちらの処理が行われたのかがわかるというわけです。

また、そこで変数の値を表示するようにしておくのも非常に有効です。

ログは本当に重要なのです(^^)


・ 関数ごとに実行する
C++の処理の単位は関数です。関数に様々な引数を与えて、戻り値を調べれば

その関数が正常に動作しているかが分かります。関数は様々な状況で

呼び出されますが、デバッグしたい部分を切り出して main関数からすぐに

実行するようなテストプログラムを作成すれば効率的です。

ここでも「困難は分割せよ」ですね(^^)


・ throw文を記述する
ある条件の時に意図的に例外を発生させることで、エラー箇所とエラー内容を

特定できるかもしれません。 例外処理であるtry, catch文でエラー内容を

取得します。


・ assertマクロを記述する
assertマクロを使用すると、特定の条件が当てはまらない時に、メッセージを

出力してプログラムを強制終了することができます。値をあらかじめ評価して

おくことで、バグの早期発見につながります。 例えば、「assert(a == 5);」と

書いておくと、a の値が 5 でなくなった時に 「assertion failed」というメッセージを

出力して強制終了します。

assertマクロを使う時は、<assert.h> のインクルードが必要です。

上記の画像のプログラムを実行すると以下の結果になります。

# ./test
 start
as: as.cpp:18: int main(): Assertion `a == 5' failed.
中止


・ 処理の流れを限定する
バグが潜んでいる箇所を特定するには、条件分岐が邪魔になることが

あります。条件分岐は状況に応じて動作が変わるのでバグの位置が

分かりにくくなってしまいます。このような場合は、条件分岐の条件を

書き換えたり、どちらか片方だけを行うようにするのも一つの方法です。

この方法は、論理的にありえないエラーケースのテストなどにも有効です。


・ データ構造を推測する
時にはこれまで紹介したような方法だけでなく、推理力が

必要になることがあります。複雑なアルゴリズムやデータ構造の

プログラムでは、バグの箇所とは全く関係ない部分で異常な動作を

示すこともあります。そのような場合、いくらコードを見返しても

「ありえない」という結論にしか辿り着けません(-_-;)

ここはひとつ、メモリがどのように使われているのかを考えてみましょう。

また、データ構造を紙に書き出してみるというのもいいでしょう。

配列の範囲外にアクセスしたり、ポインタが間違ったところを参照して

いるため、突然異常終了するというのは非常によくあることです(^^;)

ありがちだけど、発見するのが難しい厄介な問題です...


さて、ここまでは自力で解決する方法を見てきました。

次回は、ツールに頼るというデバッガ利用についてです。


今日の名言
人間は幸福を求めてこそ意味ある存在である。そしてこの幸福は、人間自身の
中にある。つまり自分が生存するために、毎日必要なものを満足させるところに
あるのだ。
                                   レフト・トルストイ

多くの人々は、どこかほかの土地へ行きさえすれば、何か他の仕事につきさえ
すれば、それですぐ幸福になれると考えているが、ちょっと考えものだ。だから
自分の今手がけていることから、出来るだけ多くの幸福を得ることだ。そして
幸福になる努力を、またの日まで延ばさないことだ。
                                   デール・カーネギー

人間は、自分で努力して得た結果の分だけ幸福になる。ただしそのためには、
何が幸福な生活に必要であるか知ることだ。すなわち簡素な好み、ある程度の
勇気、ある程度までの自己否定、仕事に対する愛情、そして何よりも、清らかな
良心である。今や私は、幸福は漠然とした夢ではないと確信している。経験と
思考を正しく用いることにより、人間は自分自身から多くのものを引き出すことが
できる。決断と忍耐により、人間は自分の健康を取り戻すことすらできる。
だから人生をそのあるがままに生きよう。そして感謝を忘れないようにしよう。
                                   ジョルジュ・サンド