メモリリークの自動検出をしてみる

VC++ のライブラリにはメモリリークの検出機能があります。関数ひとつ呼んでおくだけでアプリケーション終了時にメモリの解放漏れを出力ウィンドウに表示してくれるという便利なものなのですが、実際にこれを使うまでの過程に罠が多いのでメモしておきます。長すぎて我ながらわけがわからないので、興味のある方は最後の参考サイトだけ読んだらいいと思います。

使い方

普通に使う分には簡単です。

stdafx.h の末尾*1に :

#if _DEBUG
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif

と記述し、main 関数 (MFC であれば CWinApp::InitInstanceとか) 内部で

::_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);

としておくと、アプリケーション終了時にメモリのチェックを行ってくれるようになります。

#if _DEBUG
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif

int _tmain(int argc, _TCHAR* argv[])
{
  ::_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);

  int* leak_ptr = new int[2];

  return 0;
}

例えば、上記のようなプログラムを実行すると:

Detected memory leaks!
Dumping objects ->
c:\dbgheap.cpp(16) : {96} normal block at 0x003A3760, 8 bytes long.
 Data: <        > CD CD CD CD CD CD CD CD 
Object dump complete.

このようなメッセージが出力ウィンドウに表示され、解放していないメモリを確保したファイルと行番号を教えてくれます。

operator new をオーバーロードしているファイルは先にインクルードしておく

上記のような単純なプログラムであれば何の問題も無く使用できるのですが、プログラムが複雑になるにつれ罠が顕在化してきます。

具体的には new 演算子をオーバーロードしているファイルを crtdbg.h より後にインクルードするとコンパイルが通らなくなります。

#if _DEBUG
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif

#include <vector>

int _tmain(int argc, _TCHAR* argv[])
{(略)}

#include を追加しただけですが、このプログラムはコンパイルが通りません。これを解決するには単純に crtdbg.h より先にインクルードしてやれば良いです。

#include <vector>

#if _DEBUG
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif
(略)

こうすると vector 内部でのメモリアロケート時にデバッグ用の new を通らないのでメモリリークの検出ができなくなりますが、ライブラリの中は大丈夫だろうということにしておけばいいんじゃないでしょうか。

参考? :
VS.NET 2005 STL files will not compile if new is defined as DEBUG_NEW | Microsoft Connect

GDI+ を使用する場合

このプログラムで GDI+ を使用する場合、 GDI+ のオブジェクトを new で確保しようとすると delete 時に落ちるようになります。

#include <atlbase.h>
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")

#if _DEBUG
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif

int _tmain(int argc, _TCHAR* argv[])
{
  ::_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);

  Gdiplus::Bitmap* bmp = new Gdiplus::Bitmap(10, 10);

  delete bmp; //< (※)

  return 0;
}

上記のプログラムを実行すると (※) の箇所でエラーになります。正確には GdiPlusBase クラスの operator delete で落ちます。

GDI+ のクラスは new/delete をオーバーロードしていて、メモリの確保を DLL 内部で行うようになっていますが new が引数をひとつ取るものしか用意されていないので、実行時にデバッグ用の new が呼ばれてしまい、メモリ解放時に DLL で確保していない領域を渡してしまいエラーとなります。

これを解決するには GdiPlusBase クラスの operator new に適切なオーバーロードを追加してあげる必要があります。これは Microsoft のサイトにコードが載っていますので、まんま貰ってくると良いでしょう。


PRB: Microsoft Foundation Classes DEBUG_NEW Does Not Work with GDI+

……と思いましたがこれでは足りないので、 GdiPlusBase クラスの中に以下のコードも追加します。

  void * (operator new)(size_t nSize, int nType, LPCSTR lpszFileName, int nLine)
  {
    return DllExports::GdipAlloc(nSize);
  }

これでメモリの確保も解放も DLL 内部で行うようになります。なおこの通りに実装すると GDI+ のオブジェクトを new で作成してもログが取られないので、 delete を忘れてもリークの検出がされませんので注意してください。

MFC を使用する場合

MFC の (CObjectから派生した) クラスを使用する場合はまたさらに修正が必要です。 CObject も new をオーバーロードしていますが、これは引数を 3 つ取るものまでしか用意されていないので、 CObject から派生したクラスを new しようとするとコンパイルエラーになります。

#include <afx.h>

#if _DEBUG
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif

int _tmain(int argc, _TCHAR* argv[])
{
  ::_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);

  CFile* file = new CFile(); // error

  return 0;
}

MFCを使用する場合は new を以下のように置き換えます。

#include <afx.h>

#if _DEBUG
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#define new new(__FILE__, __LINE__) //< (※)
#endif

int _tmain(int argc, _TCHAR* argv[])
{
  ::_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);

  CFile* file = new CFile(); // OK

  return 0;
}

new の引数 (_NORMAL_BLOCK) を削っているだけです。ちなみに MFC のヘッダには

void* __cdecl operator new(size_t nSize, LPCSTR lpszFileName, int nLine)
{
  return ::operator new(nSize, _NORMAL_BLOCK, lpszFileName, nLine);
}

new 演算子が上記のように定義されていますので、結果はこれまでと変わりません。

まどろっこしいですが MFC を使うかどうかで new の定義を切り替えてやればいいんじゃないかなと思います。

*1:どこでも構いませんがここに書いておくとプログラム全体でメモリリークのチェックができて便利です