C++入門|C++のメモリリーク

newで出した戦闘力メーターを片付け忘れると、ヒープ領域に残り続ける。C++のメモリリークは、使い終えた修行装置をdeleteしないことで起こります。

C++では、new演算子を使うことで、ヒープ領域にメモリを動的に確保できます。

たとえば、int型10個分の配列をヒープ領域に確保する場合は、次のように書きます。

int* pPower = new int[10];

これは、ドラゴンボール風にたとえるなら、修行場のヒープ領域に10個分の戦闘力メーターを出現させるようなものです。

ただし、newで確保したメモリは、使い終わったらdeleteまたはdelete[]で解放しなければなりません。

配列として確保した場合は、次のようにdelete[]を使います。

delete[] pPower;

この解放処理を忘れると、確保したメモリが使われないまま残り続けます。
このように、確保したメモリを解放し忘れて、メモリが無駄に占有され続ける現象を、メモリリークといいます。

操作C++の意味ドラゴンボール風のイメージ
new int[10]ヒープ領域にint型10個分を確保する10個の戦闘力メーターを出す
pPower[i]配列の各要素にアクセスする各メーターに戦闘力を記録する
delete[] pPower配列メモリを解放する使い終えたメーターを片付ける
delete[]忘れメモリが残り続ける修行場に不要な装置が放置される

C++では、メモリを自由に扱える分、片付けも自分で行う必要があります。

newで出したらdeleteする。
new[]で出したらdelete[]する。

この対応を忘れると、プログラムの中に不要なメモリが残り、長時間動かすプログラムでは大きな問題になることがあります。

図:メモリリークはヒープ領域の片付け忘れ

この図が示していること

この図では、new int[10]によってヒープ領域に10個分のメモリが確保され、そのあとdelete[]が実行されないことで、メモリが残り続ける様子を表しています。

プログラム上では値を表示できているため、すぐには問題に見えないことがあります。
しかし、ヒープ領域には使い終わったメモリが残ったままになります。

これがメモリリークです。

メモリリークとは何か

メモリリークとは、newで確保したメモリを解放し忘れることです。

C++では、newを使うとヒープ領域にメモリが確保されます。
ヒープ領域に確保したメモリは、スタック領域のローカル変数のように、関数が終わったら自動で消えるわけではありません。

そのため、使い終わったタイミングで、プログラマーが明示的にdeleteまたはdelete[]を使って解放する必要があります。

たとえば、配列を確保した場合は、次のように書きます。

int* pPower = new int[10];

// 配列を使う処理

delete[] pPower;

ところが、最後のdelete[] pPower;を書き忘れると、pPowerが指していたメモリ領域は解放されません。

int* pPower = new int[10];

// 配列を使う処理

// delete[] pPower; がない

この状態では、プログラムがそのメモリをもう使っていないのに、ヒープ領域には確保済みのメモリが残り続けます。

ドラゴンボール風に言えば、修行が終わったのに、戦闘力メーターを修行場に置きっぱなしにしているようなものです。
誰も使っていないのに、場所だけを占有し続けます。

メモリリークが発生するサンプルプログラム

ここでは、あえてdelete[]を書かず、メモリリークが発生するプログラムを見てみます。

このプログラムでは、ヒープ領域にint型10個分の配列を確保し、0から9までの番号を入れて表示します。

しかし、最後にdelete[]を行っていないため、確保した配列メモリが解放されません。

プロジェクト/ファイル名: Chap4_07/main.cpp

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    // 戦闘力メーター配列を指すポインタ変数
    int* pPower = NULL;

    // ループ用の変数
    int i;

    // int型10個分の領域をヒープ領域に動的確保する
    pPower = new int[10];

    // 10個の戦闘力メーターに値を設定して表示する
    for (i = 0; i < 10; ++i) {
        pPower[i] = i;
        cout << pPower[i] << " ";
    }

    // 改行する
    cout << endl;

    // 本来はここで delete[] pPower; が必要
    // しかし、このプログラムでは解放処理を書いていない

    // プログラムが正常に終わったことを示す
    return 0;
}

実行結果

0 1 2 3 4 5 6 7 8 9

実行結果だけを見ると、問題なく動いているように見えます。

0から9までの値が表示されています。
エラーも出ていません。

しかし、このプログラムには大きな問題があります。

次の解放処理がありません。

delete[] pPower;

つまり、new int[10]で確保したヒープ領域のメモリが、解放されていないのです。

実行結果だけでは気づきにくい

メモリリークが厄介なのは、プログラムが普通に動いているように見えることです。

今回のサンプルでも、実行結果は次のように正しく見えます。

0 1 2 3 4 5 6 7 8 9

値も表示されています。
すぐに強制終了するわけでもありません。

そのため、delete[]を書き忘れていても、ぱっと見ただけでは気づきにくいです。

ドラゴンボール風にたとえるなら、修行メーターが正常に数値を表示したので、「問題なし」と思ってしまう状態です。

でも、実は裏側では、使い終わったメーターがヒープ領域に残り続けています。

これが、メモリリークの見つけにくさです。

メモリリークが起きている箇所

このプログラムでメモリを確保しているのは、次の行です。

pPower = new int[10];

この行によって、int型10個分の領域がヒープ領域に確保されます。

コード内容
int* pPower = NULL;ポインタ変数を用意する
pPower = new int[10];ヒープ領域にint型10個分を確保する
pPower[i] = i;配列要素に値を入れる
cout << pPower[i];配列要素を表示する

ここまでは問題ありません。

問題は、配列を使い終わったあとに、次の処理がないことです。

delete[] pPower;

new int[10]で確保したので、delete[] pPower;で解放する必要があります。

この対応が抜けると、メモリリークになります。

図:delete[]を書き忘れたときの流れ

この図が示していること

この図では、new int[10]で配列メモリを確保し、値を代入して表示したあと、delete[]を書かずにreturn 0へ進んでしまう流れを表しています。

表示処理そのものは成功しています。
しかし、ヒープ領域に確保した10個分のメモリは解放されていません。

つまり、結果表示だけでは分からない場所で、メモリリークが起きています。

正しく解放するサンプルプログラム

メモリリークを防ぐには、使い終わったあとにdelete[]で解放します。

配列をnew[]で確保した場合は、必ずdelete[]で解放します。

プロジェクト/ファイル名: Chap4_07/main_fixed.cpp

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    // 戦闘力メーター配列を指すポインタ変数
    int* pPower = NULL;

    // ループ用の変数
    int i;

    // int型10個分の領域をヒープ領域に動的確保する
    pPower = new int[10];

    // 10個の戦闘力メーターに値を設定して表示する
    for (i = 0; i < 10; ++i) {
        pPower[i] = i;
        cout << pPower[i] << " ";
    }

    // 改行する
    cout << endl;

    // 動的に確保した配列領域を解放する
    delete[] pPower;

    // 解放済みであることを分かりやすくする
    pPower = NULL;

    // プログラムが正常に終わったことを示す
    return 0;
}

実行結果

0 1 2 3 4 5 6 7 8 9

実行結果は、メモリリークがあるプログラムと同じです。

しかし、内部の動きは違います。

こちらのプログラムでは、最後に次の処理を行っています。

delete[] pPower;

この処理によって、new int[10]で確保した配列領域が正しく解放されます。

さらに、そのあとでpPowerにNULLを代入しています。

pPower = NULL;

これは、pPowerがもう有効なメモリ領域を指していないことを分かりやすくするためです。

new[]とdelete[]は必ずセット

配列メモリを扱うときは、生成と解放の対応が大切です。

確保方法解放方法
new intdelete
new int()delete
new int[10]delete[]

今回のように、配列を確保した場合は、必ずdelete[]を使います。

pPower = new int[10];

// 配列を使う処理

delete[] pPower;

delete[]を忘れると、メモリリークになります。

また、配列なのにdeleteだけを使うのも正しくありません。

delete pPower;

この書き方は、new[]で確保した配列に対して使うべきではありません。

配列を確保したなら、配列用の解放であるdelete[]を使います。

メモリリークの問題点

メモリリークは、短いプログラムではすぐに問題が見えないこともあります。

今回のような小さなサンプルでは、プログラムがすぐ終了するため、実行結果だけを見ると問題がないように感じるかもしれません。

しかし、長時間動き続けるプログラムでは大きな問題になります。

たとえば、次のようなプログラムです。

種類メモリリークの影響
ゲームサーバー長時間稼働でメモリ使用量が増え続ける
監視システムだんだん動作が重くなる
組み込み機器メモリ不足で動作が不安定になる
画像処理アプリ大きなデータの解放忘れでメモリを圧迫する

ドラゴンボール風にたとえるなら、修行施設で毎回新しい戦闘力メーターを出しているのに、使い終わったメーターを片付けない状態です。

最初は広い修行場なので問題に見えません。
しかし、何度も修行を繰り返すと、不要なメーターがどんどん積み上がり、やがて新しい装置を置けなくなります。

C++のメモリリークも同じです。
小さな解放忘れが積み重なると、システム全体のパフォーマンスが落ちたり、最悪の場合、動作が停止したりする可能性があります。

メモリリークが見つけにくい理由

メモリリークは、文法エラーではありません。

delete[]を書き忘れても、コンパイルが通ることがあります。
実行結果も、一見正しく表示されることがあります。

そのため、見つけにくいです。

問題見つけにくい理由
delete[]忘れコンパイルエラーにならないことがある
実行結果が正しく見える表示だけでは解放漏れが分からない
影響が後から出る長時間動かして初めて重くなることがある
場所が分かりにくいどのnewが解放されていないか追いにくい

C++では、メモリ管理をプログラマーが意識する必要があります。

だからこそ、newを書いたら、その近くでdeleteの責任も考えることが大切です。

図:メモリリークが積み重なると修行場が圧迫される

この図が示していること

この図では、delete[]を忘れたnew int[10]が何度も実行されることで、ヒープ領域に未解放メモリが積み重なっていく様子を表しています。

1回だけなら小さな問題に見えるかもしれません。
しかし、長時間動くプログラムで繰り返されると、メモリ使用量が増え続けます。

その結果、動作が重くなったり、メモリ不足になったりする可能性があります。

C++とガベージコレクタ

プログラミング言語の中には、使われなくなったメモリを自動的に回収する仕組みを持つものがあります。

この仕組みをガベージコレクタと呼びます。

ガベージコレクタがある言語では、プログラマーが明示的にdeleteを書かなくても、不要になったメモリを処理系が回収してくれる場合があります。

しかし、C++では、言語仕様として一般的なガベージコレクタを前提にしていません。

C++では、newで確保したメモリは、自分でdeleteするのが基本です。

項目C++の考え方
newメモリを自分で確保する
deleteメモリを自分で解放する
delete忘れメモリリークの原因になる
メモリ管理プログラマーが意識する必要がある

この点は、C++が難しく感じられる理由の1つです。

ただし、自分で細かくメモリを管理できることは、C++の強みでもあります。

必要なタイミングで確保し、必要なくなったタイミングで解放する。
この制御ができるからこそ、C++は高速な処理や細かい資源管理が求められる場面で使われます。

メモリリークを防ぐための考え方

メモリリークを防ぐためには、newとdeleteの対応を常に意識します。

特に配列の場合は、new[]とdelete[]をセットで覚えます。

書いたコード必要な対応
new intdelete
new int()delete
new int[10]delete[]
new クラス名()delete
new クラス名[要素数]delete[]

今回のサンプルでは、new int[10]を使っているので、必要なのはdelete[]です。

pPower = new int[10];
delete[] pPower;

この2つをセットで考えるようにしましょう。

また、deleteしたあとは、必要に応じてポインタにNULLを入れておくと、解放済みであることが分かりやすくなります。

delete[] pPower;
pPower = NULL;

メモリリークで押さえたいポイント

メモリリークでは、次のポイントを押さえておきましょう。

ポイント内容
メモリリーク確保したメモリを解放し忘れること
原因newしたのにdeleteしない、new[]したのにdelete[]しない
見つけにくさ実行結果だけでは分からないことがある
問題点メモリ使用量が増え続け、動作低下や停止につながる
C++の特徴メモリ管理を自分で行う必要がある
防ぎ方newとdelete、new[]とdelete[]を必ず対応させる

C++では、ヒープ領域に確保したメモリを使い終わったら、必ず解放します。

配列ならdelete[]です。

pPower = new int[10];

// 配列を使う処理

delete[] pPower;

ドラゴンボール風に言えば、newは修行場に戦闘力メーターを出現させる技です。
delete[]は、使い終わったメーターをきれいに片付ける技です。

出すだけ出して片付けないと、修行場は不要な装置でいっぱいになります。
C++のプログラムでも同じように、不要なメモリが残り続けると、システム全体に悪い影響を与えます。

メモリリークを防ぐ第一歩は、newで確保したものを最後まで責任を持って解放するという意識を持つことです。