|
動的メモリ確保とメモリリーク
今回は、動的なメモリの確保とメモリリークについてです。動的なメモリというのは、malloc()関数などで確保するメモリです。Win32の場合、スタックサイズはデフォルトで1MBですが、動的なメモリは2GBまで使うことができます。
スタックは、あくまで関数内の作業用の変数領域として使い、サイズの大きいデータや可変サイズのデータは動的に確保するのが一般的です。
さて、メモリを動的に確保する場合、メモリリークには常に気をつけないといけません。malloc()などで動的に確保したメモリは、free()で開放するまでずっと確保されたままになります。
free()で開放しなくても、プロセスが終了すれば、確保したメモリはすべて開放されますが、バックグラウンドで動き続けるサービスのようなプログラムの場合は、一箇所でも開放し忘れがあると、徐々にメモリを食いつぶしていき、最後にはプログラムが動作不能になってしまいます。
メモリリークのやっかいなところは、起動したあとしばらくは普通に動作しているように見えるので、見つけるのが難しい点です。
メモリリークはCプログラムの典型的なバグの一つです。直ちに致命的なエラーとなることはありませんが、徐々にリソースを消費していくので、時限爆弾を仕込んでいるようなものですね。
基本的には、「確保したものは、必ず開放する」ということを守ればメモリリークはおきないはずなので、これは、プログラムの作り次第でほとんどは排除することができます。メモリリークがおきるということは、プログラムの作りがまずいと思ってよいと思います。
では、ここからはメモリリークのおきないプログラムの作り方を説明していきます。
(1)確保・開放を1つのペアとして考える
まずは、基本中の基本ですが、malloc()で確保したメモリは、必ずfree()で開放します。この2つの関数は、2つで1つだと思ってください。malloc()を書いたら、必ず対応するfree()を記述します。malloc()だけ書いてfree()を書かないなんてことは、どんな場合でもありえません。
「プログラムが終了したら自動的に開放されるから、開放しなくてもいいや」なんて考えてはいけません。自動的に開放されるかされないかに関わらず、「自分で確保したものは、必ず自分で開放する」という考えを持つことが大切です。自分のプログラム内で完結するように作れば、少なくとも自分の責任でメモリリークがおきることがないことが保証できるからです。
(2)原則として、関数内で確保・開放を完結させる
動的なメモリを確保する場合、大きく分けると、プログラム全体で使うようなグローバルなデータで、サイズの大きいものを作る場合と、関数内で一時的に大きなサイズのデータを使いたい場合の2通りがあります。
関数内で確保・開放する場合は、関数の先頭で確保し、関数の最後で開放します。そして、関数からのreturnは関数の最後だけに書きます。具体的には次のような形式になります。
int func()
{
int err = 0;
int *ptr = NULL;
// 関数の先頭で確保
ptr = (int *)malloc(sizeof(int) *1024);
if (!ptr) err = 1;
// 関数の内容1
if (!err)
{
// ...
}
// 関数の内容2
if (!err)
{
// ...
}
// 関数の最後で開放
if (ptr) {free(ptr), ptr = NULL;}
return err;
}
|
メモリの空き容量不足で、動的なメモリが確保できない場合、malloc()関数はNULLを返すので、必ずmalloc()の戻り値に対してNULLチェックをします。NULLが返った場合はエラーとし、以降の処理はスキップさせます。
関数内でエラーが発生した場合は、以降の処理をスキップするようにします。よくありがちですが、エラーが発生した時点でreturnしてはいけません。returnすると、最後の開放処理が実行されなくなってしまいます。
ここで、「すべてのreturnの直前に開放処理を入れればいいんじゃないの?」と思ったでしょうか?そのような考え方はかなり苦しいです。動的に確保するメモリが後から追加されたら、すべてのreturnの前に開放処理を追加しないといけません。もし一箇所でも開放処理を入れ忘れたらどうしますか?「確保したものは、必ず開放する」ことが保証できますか?
入口と出口を1つずつにし、「入ったら即確保、出る直前で開放」とすることによって、全ての抜け道をふさぐことができるのです。これは、どんな関数でも同じです。returnを2つ以上書かないとできない処理なんてものは一切ありません。
メモリを開放したら、ポインタをNULLで初期化し直します。
「関数の最後なのに、初期化する必要なんてあるの?」
プログラム上は確かに省略可能な処理ですが、これは「使えなくなったメモリ領域をプログラム上から切り離す」という意味のある大切な処理です。このように、「意味を持たせるために、省略可能な処理をあえて書く」というのは、整然としたプログラムを書くために必要なことです。
もう一つの、関数をまたいで使うような動的メモリの場合は、細心の注意が必要です。こちらも原則として、1つの関数内に確保・開放処理をペアで記述します。プログラム全体にわたって共通で使うようなデータの場合は、main()関数の先頭で確保して、main()関数の最後で開放します。
malloc()とfree()の記述を、別々の関数に分けてはいけません。呼ばれるのか呼ばれないのかわからないような関数にfree()だけ記述するのは、あまりに危険です。これはメモリリークの元凶となります。
|