メモリ リークが C/C++ プログラムの動作不良の原因になることはよくあります。Visual C++ のデバッガと CRT ライブラリを使って、メモリ リークの原因を突き止めることができます。以下のトピックで、その方法を説明します。
メモリ リークの検出を使用可能にする
メモリ リークの検出に使われる主なツールは、デバッガと CRT デバッグ ヒープ関数です。デバッグ ヒープ関数を使用可能にするには、プログラムに以下のステートメントをインクルードする必要があります。
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include ステートメントは、ここに示した順番で挿入する必要があります。順番を変えると、使用する関数が正しく機能しないことがあります。crtdbg.h をインクルードすることにより、malloc 関数と free 関数が、それぞれのデバッグ バージョンである _malloc_dbg と _free_dbg にマップされ、これらのデバッグ バージョンがメモリの割り当てと割り当ての解除を追跡します。このマッピングは、デバッグ ビルドでのみ (つまり _DEBUG が定義されているときにのみ) 行われます。リリース ビルドは、通常の malloc 関数と free 関数を使用します。
#define ステートメントは、CRT ヒープ関数のベース バージョンを、それに対応するデバッグ バージョンにマップします。このステートメントは絶対に必要というわけではありませんが、このステートメントがないと、メモリ リークのダンプに含まれる情報の有用性が半減します。
上記のステートメントを追加したら、プログラムに以下のステートメントを追加することによって、メモリ リーク情報をダンプすることができます。
_CrtDumpMemoryLeaks();
デバッガでプログラムを実行すると、_CrtDumpMemoryLeaks により、[アウトプット] ウィンドウの [デバッグ] タブにメモリ リーク情報が表示されます。メモリ リーク情報は、以下のように表示されます。
Detected memory leaks!
Dumping objects ->
C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
#define _CRTDBG_MAP_ALLOC ステートメントを挿入しなかった場合のメモリ リークのダンプは、以下のようになります。
Detected memory leaks!
Dumping objects ->
{18} normal block at 0x00780E80, 64 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
両者を比べてみればわかるように、_CrtDumpMemoryLeaks は、_CRTDBG_MAP_ALLOC が定義されているときの方が、はるかに有用な情報を提供してくれます。_CRTDBG_MAP_ALLOC が定義されていない場合は、以下の情報が表示されます。
- メモリ割り当て番号 (中括弧に囲まれた数字)
- ブロックの型 (Normal、Client または CRT)
- 16 進数で表されるメモリの位置
- バイト単位で表されるブロックのサイズ
- 先頭の 16 バイトの内容 (16 進数)
_CRTDBG_MAP_ALLOC が定義されていると、リークしたメモリが割り当てられていたファイルも表示されます。ファイル名に続く括弧の中の数字 (下の例では 20) は、ファイル内の行番号です。行番号とファイル名が含まれている出力の行をダブルクリックすると、
C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.
メモリが割り当てられているソース ファイル内の行 (上の例では leaktest.cpp の行番号 20) にカーソルが移動します。出力の行を選択し、F4 キーを押しても同じように動作します。
このページのトップへ
CRT レポート モードの設定
既に説明したように、デフォルトでは、_CrtDumpMemoryLeaks は、メモリ リーク情報を[アウトプット] ウィンドウの [デバッグ] ペインにダンプします。_CrtSetReportMode を使ってこの設定をリセットし、ほかの場所に情報をダンプすることができます。ライブラリを使用すると、ライブラリが出力をほかの場所にリセットすることがあります。その場合は、以下のステートメントを使って、出力の場所を [アウトプット] ウィンドウに戻すことができます。
_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );
_CrtSetReportMode を使って出力をほかの場所に送る方法についての詳細は、Visual C++ ドキュメントの『ランタイムライブラリ リファレンス』の _CrtSetReportMode!href(http://msdn.microsoft.com/library/devprods/vs6/vc++/vccore/_crt__crtsetreportmode.htm) を参照してください。
このページのトップへ
メモリ割り当て番号にブレークポイントを設定する
メモリ リーク レポートのファイル名と行番号は、リークしたメモリが割り当てられている場所を示しますが、メモリが割り当てられている場所がわかっても、必ずしも問題の原因が判明するわけではありません。多くの場合、プログラムの実行中は、割り当てが何度も呼び出されますが、メモリのリークは特定の呼び出しでしか起きないかもしれません。問題の原因を突き止めるには、リークしたメモリが割り当てられている場所だけでなく、リークが発生する条件も解明する必要があります。それを可能にする情報がメモリ割り当て番号です。この番号は、ファイル名と行番号の表示に続けて中括弧の中に表示されます。たとえば、以下の出力では、「18」がメモリ割り当て番号です。これは、リークしたメモリがプログラム内で18番目に割り当てられたメモリのブロックであることを示しています。
Detected memory leaks!
Dumping objects ->
C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
CRT ライブラリは、CRT ライブラリ自身や MFC など、ほかのライブラリによって割り当てられるメモリを含む、プログラムの実行中に割り当てられるすべてのメモリ ブロックの数をカウントします。したがって、割り当て番号 N のオブジェクトは、プログラム内で N 番目に割り当てられたオブジェクトですが、コードによって N 番目に割り当てられたオブジェクトではないかもしれません (ほとんどの場合、両者は一致しません)。
割り当て番号を使って、メモリが割り当てられている場所にブレークポイントを設定することができます。これを行うには、まず、プログラムの先頭付近に位置ブレークポイントを設定します。プログラムがその位置で中断したときに、[クイック ウォッチ] ダイアログ ボックスまたは [ウォッチ] ウィンドウで、メモリ割り当てブレークポイントを設定することができます。たとえば、[ウォッチ] ウィンドウで、[シンボル名] カラムに以下の式を入力します。
_crtBreakAlloc
CRT ライブラリのマルチスレッド DLL バージョン (/MD オプション) を使っている場合は、以下のようにコンテキスト演算子を追加する必要があります。
{,,msvcrtd.dll}_crtBreakAlloc
ここで Enter キーを押します。デバッガが呼び出しを評価し、結果が [値] カラムに表示されます。メモリ割り当て位置にブレークポイントが設定されていない場合、この値は ?1 になります。[値] カラムの値を、実行を中断したいメモリ割り当て位置の割り当て番号に変更します。たとえば、上記の出力が示す割り当て位置で実行を中断するには、値を 18 に設定します。
注目しているメモリ割り当て位置にブレークポイントを設定したら、デバッグを継続することができます。割り当ての順番が変わらないように、前回実行したときと同じ条件でプログラムを実行する必要があるので注意してください。指定されたメモリ割り当て位置でプログラムの実行が中断されたら、[コール スタック] ウィンドウやその他のデバッガ情報を参照し、メモリが割り当てられたときの条件を調べます。必要な場合は、その位置からプログラムの実行を再開し、オブジェクトに何が起きるかを確認すれば、割り当てが適切に解除されない理由が判明することもあります。(注意: オブジェクトにデータ ブレークポイントを設定する方法も効果的です。)
通常は、デバッガでメモリ割り当てブレークポイントを設定する方が簡単ですが、コード内でブレークポイントを設定することもできます。コード内でメモリ割り当てブレークポイントを設定するには、以下のような行を追加します (18 番目のメモリ割り当ての場合)。
_crtBreakAlloc = 18;
代わりに、同じ機能を持つ _CrtSetBreakAlloc 関数を使うこともできます。
_CrtSetBreakAlloc(18);
このページのトップへ
メモリ状態の比較
メモリ リークの発生源を突き止める別の方法は、重要な位置でアプリケーションのメモリ状態のスナップショットを取り込む方法です。CRT ライブラリに付属している構造体型 _CrtMemState を使って、メモリ状態のスナップショットを格納することができます。
_CrtMemState s1, s2, s3;
特定の位置でメモリ状態のスナップショットを取り込むには、_CrtMemState 構造体を _CrtMemCheckpoint 関数に渡します。この関数は、構造体に現在のメモリ状態のスナップショットを格納します。
_CrtMemCheckpoint( &s1 );
_CrtMemState 構造体を _CrtMemDumpStatistics 関数に渡すことにより、任意の位置の _CrtMemState 構造体の内容をダンプすることができます。
_CrtMemDumpStatistics( &s3 );
この関数は、以下のようなメモリ割り当て情報のダンプを出力します。
0 bytes in 0 Free Blocks.
0 bytes in 0 Normal Blocks.
3071 bytes in 16 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 3071 bytes.
Total allocations: 3764 bytes.
メモリ リークがコードの特定のセクションで発生したかどうかを判断するには、そのセクションの前後のメモリ状態のスナップショットを取り込み、_CrtMemDifference を使用して 2 つの状態を比較します。
_CrtMemCheckpoint( &s1 );
// memory allocations take place here
_CrtMemCheckpoint( &s2 );
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );
関数の名前が示しているように、_CrtMemDifference は 2 つのメモリ状態 (2番目のパラメータと3番目のパラメータ) を比較し、2 つの状態の差である結果 (1 番目のパラメータ) を生成します。_CrtMemCheckpoint 呼び出しをプログラムの先頭と末尾に挿入し、_CrtMemDifference を使って結果を比べることによってもメモリ リークの有無をチェックすることができます。リークが検出された場合は、_CrtMemCheckpoint 呼び出しを使って、プログラムを分割し、バイナリ サーチ法を使ってリークの場所を突き止めることができます。
このページのトップへ