第八報:メモリリークと crtdbg.h

 先日友人から _CrtDumpMemoryLeaks という関数についてたずねられました。この関数は名前の通りメモリリークを検知してその情報を表示する関数なのですが、その情報が変だというのです

 先ずはこの関数について話をしましょう。

 この関数を使うためには crtdbg.h ヘッダファイルをインクルードする必要があります。それだけで一応使えるようになります。

プログラム
// MemLeak1.cpp
#include <crtdbg.h>

int main()
{
    new int;
    _CrtDumpMemoryLeaks();
    return 0;
}
アウトプットウィンドウ
Dumping objects ->
{16} normal block at 0x00780EC0, 4 bytes long.
 Data: <    > CD CD CD CD 
Object dump complete.

 このように、メモリリークがあるとその旨を表示し、アドレスとバイト数、そしてそのデータの内容を表示してくれます。

 しかし、これだけでは一体どのコードでメモリリークしたのかがさっぱりわかりません(new int; のところじゃないか、なんて無粋な突っ込みはなしね)。

 そのためには operator new(size_t, const char*, int) を作って new する際にファイル名 __FILE__ と行番号 __LINE__ を記録するようにさせればいいわけですが、VC++ではデバッグビルド時に _CRTDBG_MAP_ALLOC というマクロを定義してやれば自動的にそういう new が作られます(正確にはもう1つ引数があります)。MSDNを見てみると「このフラグは CRTDBG.H 中で宣言されています」と書いてありますがこれは嘘っぱちで、自分で定義する必要があります。

 で、メモリを確保するときにはこの引数付き new を使うわけですが、いちいち __FILE__ や __LINE__ を代入して呼び出すのは面倒なので、そこら辺をちゃんとしてくれるコードが crtdbg.h 中にあるらしいのです。その場合には crtdbg.h をインクルードする前に _CRTDBG_MAP_ALLOC を定義し、さらに cstdlib もインクルードする必要があるようです。

 それで確かに表示されるようになりました。しかし、その情報は次のようなものです。

プログラム
// MemLeak2.cpp
#define _CRTDBG_MAP_ALLOC
#include <cstdlib>
#include <crtdbg.h>

int main()
{
    new int;
    _CrtDumpMemoryLeaks();
    return 0;
}
アウトプットウィンドウ
Dumping objects ->
c:\program files\microsoft visual studio\vc98\include\crtdbg.h(552) :
{16} normal block at 0x00780EC0, 4 bytes long.
 Data: <    > CD CD CD CD 
Object dump complete.

 な、なんと、crtdbg.h で呼んだ new でメモリリークが起こりましたという風に出てきます。どこでやってもどんな場合でも常にこの位置です。

 これは明らかにあーいうコードを書いているに違いありません。そうです。

inline void* __cdecl operator new(unsigned int s)
        { return ::operator new(s, _NORMAL_BLOCK, __FILE__, __LINE__); }

 こーいうコードです。

 マクロの展開はプリプロセスで行われます。インライン関数の処理はコンパイル時に行われます。プリプロセスはコンパイルより先に行われるわけで、この __FILE__ と __LINE__ はインライン展開される前の、すなわち crtdbg.h での情報に置きかえられるだけなのです!

 なんやー。なんやなんやー。これやと何のためにファイル名と行数を表示できるようにしたか分からんやないかー。このコード書いたやつはアホかー。

 と思ってマクロに書き換えたところ、別のところでコンパイルエラーが起こりました。どうやら new のオーバーロードをしているところでも new(...) の形に置き換えてしまい、エラーになったようです。すみません。俺もアホでした。

 ということで、次のように new のオーバーロードを行うヘッダを先にインクルードしておいてマクロを定義する方法で落ち着きました。

プログラム
// MemLeak3.cpp
#include <cstdlib>
#include <new>
#include <memory>

using namespace std;

#include <crtdbg.h>
// crtdbg.h をインクルードしたあとに _CRTDBG_MAP_ALLOC を定義してやる
// 前でも問題ないけど、それじゃつまらんので(ぉ
#define _CRTDBG_MAP_ALLOC

#define new  ::new(_NORMAL_BLOCK, __FILE__, __LINE__)

int main()
{
    new int;
    _CrtDumpMemoryLeaks();
    return 0;
}
アウトプットウィンドウ
Detected memory leaks!
Dumping objects ->
C:\My Documents\MyProjects\Test\Test.cpp(17) :
{16} normal block at 0x00780EC0, 4 bytes long.
 Data: <    > CD CD CD CD 
Object dump complete.

 ヘッダの二重インクルードは防止されているので、これ以降にまた new や memory のインクルードがあっても問題ありません。

 本当は NEW みたいに別の名前にしておいたほうがいいのですが、友人のプログラムは既にかなり作られており、new を置換...できることはできるはずですが...まぁいちいちやりたくないだろうので、そういう形で回答しておきました。

 最もよい回答は crtdbg.h の例のアホなコードを無視して、

#if defined(_DEBUG) && defined(_CRTDBG_MAP_ALLOC) && !defined(NEW)
#define NEW  ::new(_NORMAL_BLOCK, __FILE__, __LINE__)
#else
#define NEW  new
#endif

として、new でなく NEW を使うようにすることです。あとは前回やった「ユーザー定義キーワード」に NEW を登録してしまえばちゃんと色もついて読みやすくなります。

 おそらくあのコードを書いた人も最初はマクロで書いたけど例のエラーが出て泣く泣くインライン関数で書いたのだと思いますが、なんか納得がいきませんねぇ...。

 というわけで、厄介なバグのうちの1つ「メモリリーク」を検知するこの関数 _CrtDumpMemoryLeaks をこの NEW と共に有効利用してみてください。


追記

 この _CrtDumpMemoryLeaks ですが、この関数は正確には「この関数を呼んだ時点で開放されていないメモリの情報を表示する」関数です。

 たとえ main の最後に書いておいたとしても、cin などのグローバルオブジェクトで確保されたメモリは、まだデストラクタが呼ばれておらず開放されていないわけです。これまで検知してしまうので、メモリリークしていないにもかかわらずメモリリークしたと報告してしまうことがあります。

 これを回避する方法もあるようで、_CrtSetDbgFlag という関数を使うといいようです。この関数をどこかで

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

という風に呼んでおけば、プログラムが終了したときに自動的に _CrtDumpMemoryLeaks を呼んでくれるそうです。この場合、ちゃんとグローバルオブジェクトの開放の後に呼ばれるようです。

 というわけで、_CrtSetDbgFlag の方を使ったほうが良さそうですね。

 今までこいつら使った事なかったのですが、VC++5.0だと自動的にチェックしてくれてた記憶があります。どうも、自動的にチェックされなくなったのかなー、という気はしてたのですが、今まで調べずじまいでした。何でデフォルトで呼ばないかなー、マイクロソフトよー。


目次に戻る

トップページに戻る

Last update was done on 2001.11.4