第4章 不測の事態対策
メモリリーク対策
メモリリーク
メモリリークとは、プログラムのロジックの中で動的に割り当てたメモリブロックが解放されずに残る問題である。この問題の発生が積み重なると次第に使えるメモリ資源が枯渇し、プログラムはサービス不能状態に陥ってしまう。

メモリリーク問題はソースコードの静的検査で見いだすことが難しいとされている。
なぜなら、メモリリークを生じるプログラムミスの箇所を見いだすことは、ソースコードの構文を把握し、プログラムの動作をシミュレートしてメモリブロックのライフサイクルをたどる必要があるからである。
しかしながら、メモリリーク対策を施す一定のプログラミングスタイルが存在し、ソースコード中でそのスタイルが用いられているか否かを機械的に検査することはそれほど複雑なことではない。
本稿では、そのようなプログラミングスタイルを取り入れることを提案すると共に、そのスタイルが用いられているか否かの検査をツールによる自動化を視野に入れて整理している。
メモリリークの例
動的メモリの解放もれがおこるケースとして、例えば、次のような事例が挙げられる。:
(1) 単純なポインタ上書き
すでにメモリブロックを指しているポインタに新しいメモリブロックへの参照を上書きする。
p = malloc(size1); ←(A)
...
if (size1 is not enough) {
p = malloc(size2); ←(B)
}
(A)の時点で確保されたメモリブロックへの参照が、(B)の代入により失われ、このメモリブロックは解放される機会を失う。
本来は次のようにすべきであった。
p = malloc(size1);
...
if (size1 is not enough) {
free(p); ←(追加)
p = malloc(size2);
}
※ malloc() ではなく、領域が安全にクリアされる calloc() を使うべきだという意見があるかもしれない。それには一理ある。しかし、ここの議論では最もプリミティブな malloc() で進めさせていただく。
(2) ループによるポインタ上書き
ループ中で動的メモリを割り当てて処理を行う際、メモリブロックの解放を忘れている。
for(...) {
...
p = (sometype*)malloc(...);
...
p に対する処理
...
}
ポインタに割り当てられたメモリブロックは次々と参照不能となり、メモリリークが起こる。
単純に考えると、次のような修正が考えられる。
for(...) {
...
p = (sometype*)malloc(...);
...
p に対する処理
...
free(p); ←(追加)
}
改良コードをもうひとひねり
上記のループの修正例はいくつかの問題を見逃している。
例えば、ループに入った直後、malloc() 呼び出しの時点で代入する先のポインタは既に別のメモリブロックを指している可能性がある。
また、追加した free() 呼び出しを通らずに for ループが終了してしまう懸念がある。
このような問題にも対処できるようにするには、次のように書くことが望ましい。
sometype* p = NULL; ←(追加)
for(...) {
...
if (p != NULL) ←(追加) ※注
free(p); ←(追加)
p = (sometype*)malloc(...);
...
p に対する処理
...
}
if (p != NULL) { ←(追加)
free(p); ←(追加)
p = NULL; ←(追加)
} ←(追加)
※注 free()関数は、与えられた引数がNULLであっても問題を起こさないが、
「NULLの場合は何もしない」旨を明示する目的でこのif文を記述している
行儀の良い動的メモリコード
「行儀の良い動的メモリコード」とは次のようなコードである。
(1) ポインタの初期化
ポインタ変数には初期値 NULL を与える。
sometype* p = NULL;
(2) 解放を伴う動的割当
動的メモリ割当に先立って、代入先ポインタのメモリ解放を行う。
if (p != NULL)
free(p);
p = malloc(...);
memset(p, 0, ...);
割り当てたメモリブロックはクリアしておく。
callocを使う方法もある。
(3) ポインタのクリーンナップ
一連の処理の最後でポインタ変数のクリーンナップ処理を行う。
if (p != NULL) {
free(p);
p = NULL;
}
「行儀の良い動的メモリコード」の機械的な検証
任意のプログラミングスタイルの動的メモリ割当・解放ロジックの中からメモリリーク問題を見いだすことは多大の労力を要する。
それに比べると、プログラムに「行儀の良い動的メモリ割当・解放」を行わせる前提で、問題箇所を見いだすことは、比較的局所的なソースコードの特徴を確認すれば良く、要する労力が少ない。
※ この方式では「ダブルフリー」問題も防止できる。
「行儀の良い動的メモりコード」が実践されていることの検証は、次の3つの観点から行う。
(1) ポインタの初期化が行われているか
すべてのポインタ変数は、宣言されるとともにNULLで初期化されているか。(仮引数は含まない)
sometype* p = NULL;
sometype* p; ... p = NULL;
(2) 解放を伴う動的割当が行われているか
malloc/calloc/vallocの呼び出しの直前で代入先ポインタのメモリ解放が行われているか。
if (p != NULL)
free(p);
p = (sometype*)malloc(...);
/* free していない */ p = (sometype*)malloc(...);
(3) ポインタのクリーンナップ
ポインタ変数が宣言されているスコープの中の処理の最後のタイミングでポインタ変数のクリーンナップが行われているか。
a) ポインタ変数が自動変数の場合
次のふたつともが成立すること。
- 関数の出口付近でポインタのクリーンナップが行われていること
- 関数のロジックは必ずそのクリーンナップロジックを通過して関数から復帰すること
b) ポインタ変数がC++クラスのメンバ変数の場合
次が成立すること。
- クラスのデストラクタ、もしくはデストラクタが直接・間接に呼び出す関数の中で、クリーンナップが行われていること
c) ポインタ変数が関数外のstaticの変数の場合
次の4つともが成立すること。
- そのソースコードの中にクリーンナップのロジックを含む、複雑さの少ない、小さな関数が存在すること
- その関数が呼ばれると必ずクリーンナップロジックがはたらくこと
- 全ソースコードの中で、その関数が少なくとも1回、確実に呼び出されること
- 全ソースコードの中で、その関数の呼び出しより後には当該ポインタ変数にアクセスする当該ソースファイル中の関数への呼び出しがおこらないこと
d) ポインタ変数が関数外のstaticでない変数の場合
本来このような変数を使うべきでない。こうした変数の使用そのものを警告の対象とすべきである。
その上で、クリーンナップ処理が行われるか否かを次の4つともが成立するか否かで判定する。
- 全ソースコード中のどこかに当該ポインタ変数のクリーンナップのロジックを含む、複雑さの少ない、小さな関数が存在すること
- その関数が呼ばれると必ずクリーンナップロジックがはたらくこと
- 全ソースコードの中で、その関数が少なくとも1回、確実に呼び出されること
- 全ソースコードの中で、その関数の呼び出しより後には当該ポインタ変数へのアクセスがおこらないこと
まとめ
メモリリークは、長時間稼働し続けなければならないプログラムをサービス不能に陥らせる要因のひとつである。この問題に対処する際、ソースコード中の malloc() - free()、new - delete の対が崩れないよう注意するだけでなく、「迷子のブロック」が生じないよう、ポインタの取り扱いに一定のスタイルを持ち込む方法がある。