第三報:蛾退治
2000年問題も特に大きな問題は起こらなかったと安心したところで、今日(2000年2月29日)キャッシュディスペンサーや気象庁などでいろいろ問題が起こったそうです。(晴れとるのに降水量900ミリって何やねん!)どうやら「今年は閏年でない」という風に組まれていたプログラムが誤動作したようで、中途半端な知識は危険だなー、と思いました。
閏年というのは公転周期が365日ぴったりでないことから導入されとるわけやけど、よくよく考えてみれば4年に一度閏年を設けたくらいでこの問題をきれいに解決できるはずもないわけで。そこで、400年に3回だけ閏年をなくし、さらに閏秒を設けて細かい調節を行っとるわけです。
この400年に3回というのは、100の倍数の年のうち400の倍数でない年を閏年でなくす、というもの。このプログラムを組んだ人は100の倍数の年は閏年ではないことを知っとったよーですが、400の倍数の年は閏年であることまでは知らなかったということです。今年はその400の倍数の年で、見た目ごく普通の閏年のようで、大変珍しい閏年というわけなのです。(でも、どっちかというと、100年に一度の非閏年の方が珍しさを感じるんやけどね。と言いつつこの珍しい日にHPを更新しようと思ったあたり小市民。)
バグが早めに見つかってかえって良かったのか、それとも余計な知識はなかった方が良かったのかと言いたいところやけど、やはりきちんとこういうことは調べてプログラムを組むのが一番だったと言うのが正しいのでしょうね。分からないこと、曖昧なことは素直に調べるのがやっぱり一番です。はい。
さて、プログラミングツールはただのエディタ付きコンパイラではなく、バグをなくすために、作業効率を上げるためにいろいろ機能が付いとります(急に話変わるなー)。行単位に実行して変数の中身を調べたり、エラーの出た箇所を見つけたりなんていうのは、これらの機能を使えば結構簡単に出来ます。これらの機能を使わないことは、メモ帳でプログラムを書いて、コンパイルして、普通に実行して、何でエラー出るかなーと悩むようなことと変わりありません。どんどんデバッガなどの機能を使って慣れていくことはとても大事なことです。今回はデバッグの方法についてちょっと話してみたいと思います。
先ずはコンパイルエラーです。先ずなくすべきもので、これがあったら実行すら出来ません。
コンパイルエラーというのは機械的に判定したもので、あまり的確な判断はしてくれないこともままあります。セミコロン1つ忘れただけで何十何百というエラーをはくこともあります。ここで重要なことは、エラーの数に惑わされてはいけないということです。こういう場合、一連のエラーの一番はじめの部分に本物のエラーのあることが多いです。機械的に解析不可能な状態に陥ると、あとは連鎖反応的にエラーが発生します。適切に復旧してくれればそうもなりませんが、それにも限界はあるということです。その場合、エラーの始めに起こったところに本物のエラーがあることは予想に難くありません。
一度に沢山のエラーを吐くようなエラーにはいくつかのパターンがあります。型が定義されていない、カッコの整合性が取れていない、ヘッダファイルに関数の定義を書いたとかいうところです。
構造体を定義しているヘッダファイルをインクルードし忘れると、先ず構造体名が構造体名と認識されません。コンパイラはここで int 型の変数を定義したのだと解釈してとりあえず先に進みます。すると次に変数名が出てきますが、次に変数の定義が終わった後にあるべきセミコロンがないと言われます。無茶苦茶やなー。そして、次にこの変数名も int 型の変数と認識されます。すると、この構造体を使用している所全てがおかしくなってしまいます。この構造体を使用している行のエラーは読み飛ばしてしまいましょう。
カッコの整合性ですが、カッコはカッコでも重要なのは中カッコ { } の方です。これの整合性が取れていないと、特に関数定義の判定が無茶苦茶になり、収拾がつかなくなってしまいます。そのファイルの一番頭でこれをやってしまうと、見るのも嫌な結果になってしまいます。このファイルの以降のエラーの内容を読むことに全く意味はなく、カッコの整合性をとることに集中してください。
最後のヘッダファイルに関数の定義を書いた、ですが、これはコンパイルエラーではなくリンクエラーが沢山出ます。沢山のファイルにインクルードされるようなヘッダファイルでこれをやってしまうと、インクルードされた回数−1個のエラーが出ることが保証されます(笑)。エラーの内容はもちろん関数の二重定義です。大体は inline の付け忘れでこのエラーは出るもので、自分も未だに何回もやってしまいます(汗)。
あとはヘッダファイルに二重定義防止コードを書き忘れて二重定義がなんぼも出てくるとか、ミスは1つでもエラーが沢山出てくるものがあります。コンパイルに異常に時間のかかった昔ならともかく、今の環境なら上のようなエラーをつぶしたらとりあえずコンパイルし直してしまうのも手かもしれません。前のエラー情報は消えてしまいますが、直したファイルのみをコンパイルすると時間の短縮になったりします。そうやって本当に直っているか確かめ、直ったところで全体をコンパイルします。また、エラーが出たところでコンパイルを一旦中断してしまうのも手です。特にヘッダファイルのエラーと予想されるなら、早めに止めておいた方が時間の短縮になるでしょう。
コンパイルエラーにはいろいろありますが、エラーの内容をよく読んでおくことは重要です。表面的な意味もそうではあるのですが、実際にどこが悪かったのかを同時に覚えておくことが大事です。エラーの内容が適切だとは限らないわけで、ここら辺の経験を積んでおくとエラーの適切な復旧が早くなります。
さぁ、コンパイルが通ったからといって安心できません。実行してみたらエラーがいろいろ出てくるのが普通です。コンパイルエラーなどははっきり言ってバグとは言えず、ここからが本当のデバッグになります。
デバッグを支援するために、プログラミングツールにはデバッガが付いているのが普通です。「デバッガの全機能を使いこなせ」とは言いませんが、プログラムをするならある程度は使いこなせるのが当然だと思って下さい。
基本的な機能は「デバッグ文字列」「ブレークポイント」「行単位での実行」「変数内容のウォッチ」「コールスタック」です。
「デバッグ文字列」は最も単純なものです。MFCでは TRACE マクロ、SDKでは OutputDebugString 関数、_CrtDbgReport 関数などを使うことによって、デバッグウィンドウに文字列を表示することが出来ます。これがデバッグ文字列です。これを利用すると、変数の内容を表示して値のチェックをしたり、ある関数のどの部分で実行が止まるかをチェックしたりすることができます。
「ブレークポイント」は実行を途中で止めるためのものです。ブレークポイントで実行を止めても、また実行を再開することが出来ます。また、条件付きのブレークポイントもあり、条件を満たしたときにだけ実行を止めることが出来ます。また、ブレークポイントとは別に、現在実行している部分で中止する機能もあります。
ブレークポイントで実行を止めると、そこから「行単位での実行」「変数内容のウォッチ」「コールスタック」などの機能を利用することが出来るようになります。また、ブレークポイントがなくても、各種エラーによって実行が止まることがあります。大体はこのエラーでのブレークした位置からバグを突き止めます。そこからバグの位置を特定するようにブレークポイントを設定します。
「行単位での実行」は、まさに行単位で実行を行います。「カーソル位置まで実行」したり、「関数外」に出たり、「関数内」に入ったりすることもできます。その時に同時に「変数内容のウォッチ」を行うのが普通です。デバッガには変数の内容を表示する機能が付いているのが普通です。行単位で実行し、どう変数が変わって、どこでおかしくなったかをチェックするのです。
「コールスタック」は現在の関数がどの関数から呼ばれたかという履歴です。仮引数の内容がおかしかった場合などに、どの関数からおかしな値が渡されたのかチェックすることが出来ます。
デバッグも慣れてくると、#ifdef を利用する方法もあります。Visual C++ ではデバッグモードに _DEBUG というマクロが定義されます(メニューから「プロジェクト」→「プロジェクト設定」→「C/C++」→「カテゴリ:プリプロセッサ」→「プリプロセッサの定義」とたどれば、_DEBUG の定義が見つかります)。他のツールでも似たようなものが定義されているはずです。されていなければ、するまでです。あとはこれを利用してデバッグモードでのみ動作する文などを作ることができます。
先ずは以下の関数を見て下さい。
void Func(char* p) { if(p == NULL) exit(1); strcpy(p, "test"); }
この関数は "test" という文字列を格納するだけの関数ですが、引数に NULL が渡されては困ります。そこで、p が NULL の時はプログラムを強制終了するようにしています。しかし、バグさえなければここに NULL が代入されない保証があれば、デバッグが済んでしまえばこの文は意味をなくします。それどころか、if 文が実行されるだけ時間の無駄です。そこで、デバッグ時にだけこの判定を有効にする方法があります。それは以下の通りです。
void Func(char* p) { #ifdef _DEBUG if(p == NULL) exit(EXIT_ERROR); #endif strcpy(p, "test"); }
#ifdef というのは、「この後ろにある名前が定義されていれば #endif までをコンパイルする」という命令です。つまり、_DEBUG が定義されていれば判定が行われ、定義されていなければ判定は行われないということです。#ifdef は実行時の分岐を行う命令ではなく、コンパイル時の分岐を行う命令です。このようにコンパイラに指令を送る命令をプリプロセッサディレクティブ(プリプロセッサ指令)と呼びます。プリプロセッサディレクティブの頭には # が付いています。つまり、#define も #include もプリプロセッサディレクティブです。
_DEBUG が定義されていないときは間の行がコンパイラに無視され、この位置に何も書いてないことと同等になります。従って if 文を実行するタイムロスをなくすことができるのです。
そして、これを発展させるとデバッグ時のみ動作する関数(マクロ)を作ることができます。上記の値のチェックにはMFCでは ASSERT 、SDKでは _ASSERT というマクロを使用します。これらは _DEBUG が定義されていないときには無視されます。こういったものの作り方を説明しましょう。
関数を無視させる方法には2つあります。1つは関数と同じ形のマクロを作り、デバッグモードではその関数を呼び、そうでないときは何もしないというものです。もう1つは NULL 関数を利用することです。
先ずは前者です。
#ifdef _DEBUG void DebugPuts(const char* str); #define DEBUGPUTS(str) DebugPuts(str) #else #define DEBUGPUTS(str) #endif
#else というのは #ifdef の時に使用する else です。こうすると _DEBUG が定義されている時には DebugPuts が実行されますが、_DEBUG の定義されていないときには DebugPuts は実行されないことが分かります。
しかし、これでは printf のような引数の個数が可変のものが作れません。そこで後者が活躍します。
#ifdef _DEBUG void DebugPrintf(const char* str, ...); #define DEBUGPRINTF DebugPrintf #else #define DEBUGPRINTF 0 #endif
関数のカッコの前に置くのは厳密には関数名ではなく関数のアドレスです。ではここに0を置くとどうなるのでしょうか? 実はこの関数はコンパイラに無視されます。もしかしたらこのことはコンパイラ依存かもしれません。その時は、
#include <stdio.h> int main() { int a = 0; 0(a++); printf("%d", a); return 0; }
を実行してみて下さい。0と出力されれば無視され、1と出力されれば無視されなかったことになります。Visual C++ では無視されました。
これを利用すれば、可変個数の引数でもデバッグ時のみ動作する関数を作ることができます。
これらの機能を利用すればかなりデバッグは楽になります。とはいえ、利用しないよりは楽になるわけであって、それでも辛い場面もあります。しかし、これらの機能を使わなかったら、簡単なバグさえもなかなか直すことが出来ません。デバッグはプログラムの中でも重要な位置を占めています。その技術を磨くことも大切であることを認識し、これらの技術、もしくはさらなる技術を得るべく邁進しましょう!
え? タイトルの「蛾退治」ってのはどうしたのかって? えーと、バグという言葉は、コンピューター内に蛾が入ってコンピューターが止まったという事件から生まれたそうです。そこで、デバッグのことを「蛾退治」と言ってみたということでした。いまいち記憶が曖昧なので違ってるかもしれません。「この語源、間違ってるよ」っということであれば一報下さい。
Last update was done on 2000.2.29