ショートカット
ファシリテーター × あり方
コーディングの向こう側
Hello, ANOTHER world!
オブジェクト指向のはなし
プログラミングのはなし
C言語実力診断クイズ
eSkillBooks
プログラミングのはなし

メモリリークをチェックする

メモリリークは、ソフトウェアを作る上でもっともやっかいな問題です。メモリを正確に管理するのはとても難しいことです。市販されているソフトウェアがときどきフリーズするのは、多くの場合、メモリの管理方法が不完全だからではないかと思います。

メモリの使用状況を監視する機構を取り入れて、メモリリークを早期発見することができます。メモリを確保するたびに、メモリ確保ルーチンの呼び出し元のソースコード上での位置(ソースファイル名と行番号)と、確保したメモリブロックの先頭アドレスを記録していきます。これは構造体のリストを使えば簡単でしょう。そして、メモリ解放時に該当する先頭アドレスの記録をリストから削除します。これで、現在確保されているメモリブロックの情報を常に手元に置いておくことができます。

プログラムの終了時に、さきほどのリストに残っているノードをチェックして、もし残っていればその情報を出力します。出力された内容がメモリリークです。もちろんプログラム終了時には OS がヒープを掃除してくれるので、この時点で解放されていないメモリブロックがあるのは問題ないと言えばないのですが、もしあたながプロの SE なら、自分で確保したメモリはすべて自分で解放しなければなりません。わざと解放しないメモリと解放し忘れたメモリを区別するのが困難だからです。すべてのメモリを自分で解放するという方針にしたがうことによって、プログラム終了時に残っている未解放のメモリブロックをメモリリークであるとみなすことができるのです。

どうやったらいいか、もうピンときている人もいるでしょう。そう、デバッグコードを書くのです。開発中はデバッグコードによってメモリを監視し、リリースする時には監視を解除します。

メモリ監視機能付き malloc を作る

メモリブロックの情報を記録するためには、独自のメモリ確保ルーチンが必要です。プロトタイプは次のような感じになります(関数名は自分で決めて下さい!)。

void *MyMalloc_Rep( size_t sz, const char *pcFileName, int nLine );

第1パラメータは確保するメモリブロックのサイズで、オリジナルの malloc と同じものです(実際にメモリを確保するとき内部でオリジナルの malloc に渡します)。残りの2つのパラメータは呼び出し元のソースコード上の位置を特定するためのソースファイル名と行番号です。

このままでは使いづらいので、malloc と同じ使い方ができるように次のようなマクロを定義します(マクロ名は自分で決めて下さい!)。

#define MyMalloc(S) MyMalloc_Rep( (S), __FILE__, __LINE__ )

メモリ監視機能付き free を作る

上記の MyMalloc に対応する解放ルーチンが必要です。プロトタイプは次のようになります(関数名は自分で決めて下さい!)。

void MyFree( void *pv );

使い方はオリジナルの free と同じです。

メモリリーク情報表示のタイミング

もう一つ重要な問題が残っています。それは、プログラム終了時のチェックをどうやって実装するのかということです。main 関数の最後でチェックしても、別の場所で exit されてしまったらどうしようもありません。

ANSI-C には atexit という便利な関数があるので、これを使います。メモリリークがないかリストを走査する関数をつくり、これを atexit で登録します。すると、プログラムが exit するときに(もちろん main 関数が終了するときにも)登録された関数が実行されます。

なお、atexit で登録された関数は、abort 時には実行されません。

C++ の場合

C++ では通常 malloc/free を使いません。new/delete を使います。また C++ ではオーバーロードが可能なので、new/delete の定義を変えることができます。しかしグローバルの new/delete は、どこで使われているかわからないので置き換えない方が無難です。

そこで、すべてのクラスから共通に利用されるスーパークラスを用意します(普通はすでにそういう設計になっているでしょう)。そして、その new/delete を上記の MyMalloc/MyFree のように定義します。そうすると、このクラスから派生するすべてのクラスでメモリ監視機構が働きます。

なお、スーパークラスの new を

void *operator new( size_t sz, const char *pcFileName, int nLine );

と定義し、呼び出しを簡単にするために

#define new new(__FILE__,__LINE__)

としたら、グローバルの new にも

void *operator new( size_t sz, const char *pcFileName, int nLine );

という定義が必要です。これはデバッグ時のみ必要なダミー関数で、実際には単純にグローバルの標準の new を呼び出すだけです。

ひとつ注意が必要な点があります。それは、配列に対して new を使ったときは、必ずグローバルの new が呼び出されるということです。delete も同様です。この場合、メモリチェック機構は働きません。

なぜそんなことをするのかって?

どうしてこんな面倒くさい思いまでしてメモリチェック機構をつくるのでしょう?それは、楽をするためです。一度この機構を作ってしまえば、あとはプログラムが勝手にメモリリークを報告してくれます(完全に、とまでは言いませんが、ほとんどのメモリリークが自動的に検出されます)。楽でしょう?

(「プログラミングのはなし」は1998年1月から1999年1月にかけて作成されたコンテンツです。)