VC++の使い方

VC++の使い方 > チュートリアル > デバッグ入門 > Win32アプリケーションのデバッグ方法
このページの内容
Win32のデバッグ
MessageBoxを使う
デバッグ用関数を使う(1):OutputDebugString
デバッグ用関数を使う(2):DebugBreak
使い分け
ありがたいスポンサー様
All About ソフトウエアエンジニア
ネットで8%割引!自動車保険はアメリカンホーム・ダイレクト
人生の「チャンス」と「ピンチ」にモビット!
保険料が一生上がらない、保険料最大50%割引の一生涯の医療保険!

Win32のデバッグ

Win32アプリケーションでは、デバッグ情報をコンソールに出力するようなこともできません。手軽なMessageBox関数を使う方法と、デバッグをサポートするAPIについて解説していきます。

MessageBoxを使う

MessageBox関数は簡単なデバッグには便利です。例えば

MessageBox( NULL, "呼ばれたよ", "title", MB_OK);

とすれば、次のようなダイアログが表示されます。

メッセージボックスの例
メッセージボックスの例

MessageBoxで現れるダイアログはモーダルなので、ダイアログを閉じるまで制御は帰ってきません。例えば、アプリケーションが異常終了してしまう場合は、原因となりそうな周辺に1行づつMessageBox関数を挿入すれば、どこで異常終了が発生しているかを判断できます。


ただし、MessageBoxには、printf関数のように変数の値を表示するにはちょっと不便です。ということで、次のようなラッパー関数を定義するとよいでしょう。<stdio.h>をインクルードするのを忘れないようにして下さいね。

void DebugMsgBox( LPCSTR pszFormat, ...)
{
	va_list	argp;
	char pszBuf[ 256];
	va_start(argp, pszFormat);
	vsprintf( pszBuf, pszFormat, argp);
	va_end(argp);
	MessageBox( NULL, pszBuf, "debug info", MB_OK);
}

DebugMsgBox( "i : %d", 10);などとして、printf関数を使うようにして呼び出せます。


これで多少便利になりましたが、依然MessageBox関数を使ったデバッグには不便さを感じます。何といっても呼び出す度にプログラムがとまってしまうので、ループの中でのデバッグや大量のデバッグ情報を出力するのには不向きです。そもそも、MessageBox関数はデバッグ用の関数ではないですし…。ただ、手軽なデバッグ手段としてよく用いられる手段ですので、覚えておいて損はないでしょう。

デバッグ用関数を使う(1):OutputDebugString

デバッグ用に用意されているAPIにOutputDebugStringは、デバッガが存在するときにデバッグメッセージを出力します。

例えば、次のようなコードをビルドして、デバッガから起動して下さい。デバッガを起動するには、メニューから 『ビルド→デバッグの開始→実行』 を選択して下さい。F5キーを押してもOKです。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, 
		   LPSTR lpCmdLine, int intCmd)
{
	OutputDebugString( "hoge\n");
	return 0;
}

デバッガを起動すると、アウトプットのデバッグのタブに出力されます。次のような感じです。

OutputDebugStringの結果
OutputDebugStringの結果


OutputDebugStringは、デバッグモード・リリースモードに関係なく、デバッガが存在すれば文字列を出力します。この関数を埋め込んだままアプリケーションを公開するとパフォーマンスが落ちそうですが、デバッガが存在しないときにはパフォーマンスはほとんど低下しません。また、多くの市販アプリケーションの出荷版にもOutputDebugStringが埋め込まれたまま出荷されています。このようにしておけば、出荷後の環境で不具合が発生したときに、どのような状態になっているかを発見できるからです。

ただし、デバッグ効率よりも実行効率を重視する場合は、デバッグ版でビルドしたときのみOutputDebugStringを呼び出すようにしたいでしょう。これを実現するには、次のようなマクロを定義すればOKです。。

#include <windows.h>
#include <stdio.h>

#if defined(_DEBUG) || defined(DEBUG)
// Debugのとき
#define TRACE(x)   OutputDebugString(x)
#define TRACE0(x)   OutputDebugString(x)
#define TRACE1(x, a)            MyOutputDebugString(x, a)
#define TRACE2(x, a, b)         MyOutputDebugString(x, a, b)
#define TRACE3(x, a, b, c)      MyOutputDebugString(x, a, b, c)
#define TRACE4(x, a, b, c, d)   MyOutputDebugString(x, a, b, c, d)
#else
// Releaseのとき
#define TRACE(x)
#define TRACE0(x)
#define TRACE1(x, a)
#define TRACE2(x, a, b)
#define TRACE3(x, a, b, c)
#define TRACE4(x, a, b, c, d)
#endif

void MyOutputDebugString( LPCSTR pszFormat, ...)
{
    va_list	argp;
    char pszBuf[ 256];
    va_start(argp, pszFormat);
    vsprintf( pszBuf, pszFormat, argp);
    va_end(argp);
    OutputDebugString( pszBuf);
}

TRACEマクロを使えば、printf関数を使うようにデバッグ用の文字列を出力できます。注意して欲しいのは、デバッグモードでビルドする場合は_DEBUGが定義されているということです。

これは、メニューの 『プロジェクト→設定→C/C++タブ→カテゴリ:一般→プリプロセッサ』 の欄に、デバッグモードのときは_DEBUG、リリースモードのときはNDEBUGが定義されていることから分かると思います。デバッグ・リリースのモードについて知らない人は、アクティブプロジェクトと構成をご覧下さい。

デバッグ用関数を使う(2):DebugBreak

DebugBreak関数は、引数も戻り値もない関数です。アプリケーション実行時にこの関数が呼び出されると、次のようなウインドウを出してプログラマや利用者に例外を通知します。

DebugBreak関数が呼び出されると
DebugBreak関数が呼び出されると

[OK]を押すとプログラムは終了します。[キャンセル]を押すと、例外が発生した場所でプログラムが停止してデバッガが起動します。また、デバッガからアプリケーションが起動された場合は、この関数が呼び出されると呼び出された箇所でプログラムが停止します。デバッガでプログラムが停止すると、各変数の値などを参照したり、1行ずつ実行することができます。DebugBreakが呼ばれると、デバッガを使って問題の原因追及ができます。(デバッガの使い方に関しては、次頁?を参照して下さい)


この関数、役に立たないように見えて実はデバッグを何十倍も効率的にしてくれます。プログラミングしていて、本来できて欲しいはずのことがうまく行かないことがよくあります。フォントがなかったり、メモリが確保できなかったり、ネットワーク通信ができなかったり、ファイルに書き込めなかったり…、数え上げればきりがありません。しかも、複雑なプログラミングでは、小さなエラーははっきりとは現れず、1つのエラーが次のエラーを引き起こしてしまって、根本の原因を追及しにくいものです。

そのような場合のためにも、次のようなマクロを定義しておくと便利です。MyOutputDebugStringは、上と同じものを使用して下さい。

#include <windows.h>
#include <stdio.h>

#if defined(_DEBUG) || defined(DEBUG)
// Debugのとき
#define ASSERT(x) \
	if (!(x)) { \
		MyOutputDebugString("Assertion failed! in %s (%d)\n", \
			__FILE__, __LINE__); \
		DebugBreak(); \
	}
#define VERIFY(x)   ASSERT(x)
#else
// Releaseのとき
#define ASSERT(x)
#define VERIFY(x)   x
#endif

ASSERTとVERIFY、さらに上でとりあげたTRACEは、MFC では初めから定義されています。そして、実際に上のような定義のされ方をしています(MFCはオープンソースなので、興味のある方は調べてみて下さい。[VisualStudioのフォルダ]\VC98\MFC にソースがあります)。


このASSERTとVERIFYは、デバッグ版でビルドしたときは振る舞いは同じです。カッコの中の値を評価して、偽(0)ならばDebugBreakを実行して例外を発生させます。そのときに、OutputDebugStringを使って、例外の発生位置をデバッグ情報として出力します。

リリース版でビルドした場合、ASSERTは、中身ともども消え去ります。VERIFYはカッコの中身に置き換わり、カッコの中がコンパイルされます。ただし、カッコの中の評価を行わないので、結果が偽であっても例外を表示したりプログラムを停止したりすることはありません。

ASSERTは、取得したポインタやハンドルが妥当なものであるかを判断するときに便利です。引数が偽であればブレークポイントになると思えば分かりやすいのではないでしょうか(ブレークポイントについては次頁?で解説します)。それに対して、VERIFYは、呼び出した関数が正しく実行されたか(TRUEを返したか)を判断するときに便利です。次にそれぞれの例を示します。

HDC hdc = GetDC();
// リリース版だと次の行は削除される
// デバッグ版だと hdc がNULLの場合に停止する
ASSERT(hdc);
HDC hdc;;
// リリース版だと次の行は hdc = GetDC();となる
// デバッグ版だと hdc = GetDC(); を実行し、hdc が NULL の場合に停止する
VERIFY(hdc = GetDC());

使い分け

デバッグと言ってもいろんなアプローチがあります。MessageBoxを使う方法は手軽ですが、リリース版にMessageBoxが入っていると思わぬ挙動をしてしまって焦ることになります。にちゃんねるのプログラマー板、俺はバグでこんなすごい被害を出したぞ!スレには次のような投稿がありました。

確認ダイアログのメッセージを武士言葉にして開発してたものを直すの忘れていて、納品時のチェックの時に”〜よろしいでござるか?”とお客の前で表示された。”よろしいでござるよ”とフォローしてもらえてよかった。汗かいた。

この場合は確認ダイアログですが、デバッグ用ダイアログなんてもっと適当に作ると思います。気楽に入れた分だけ忘れがちになるのも事実ですので注意して下さいね(笑)


さて、次回はデバッガの使い方を解説してみたいと思います。今回のデバッグ用関数でデバッガを呼び出したはいいけど、使い方が分からなかったらどうしようもありませんもんね。