メモリリークとは? 事の起こり
動的領域に確保したメモリを解放し忘れることです(笑)。極論ですが、C言語では、mallocしたらfreeを守っておけばメモリリークを防げます。C++でも同様にnewしたらdeleteを徹底するとかスマートポインタを使うとかすればメモリリークは防げます!! ・・・と思っていた時期が私にもありました。実際はこれだけでは防げません(泣)。C++には言語仕様に根ざしたメモリリーク要因が多数存在するからです。今回は、多数の要因の中から、仮想デストラクタに起因するメモリリークをご紹介します。
抽象クラスを利用した時のメモリリーク
今回皆さんに紹介するコードはこちらです。このコードの振る舞いは非常に簡単です。クラスの構成は抽象クラスmyAbstractを派生クラスのmyConcreatに継承しているだけです。main関数では無限ループ内で派生クラスのインスタン生成し、抽象クラスでキャストした後、deleteで解放を繰り返しています。
#ifndef myClass_hpp
#define myClass_hpp
#include <stdio.h>
#include <iostream>
#include <vector>
using namespace std;
//抽象クラス
class myAbstract{
public:
virtual void method1(void) = 0;//仮想関数の定義
};
//抽象クラスの派生クラス
class myConcreat: public myAbstract{
private:
//そこそこサイズの大きいメンバを用意
vector<long> B{1000,2000,3003,4000,50000,50000};
public:
void method1(void){
cout<<"Hello World"<<endl;
};
~myConcreat(){//デストラクタ
//本クラスが解体された時のメッセージを表示
cout<<"派生クラスは解放済"<<endl;
}
};
#endif /* myClass_hpp */
#include <iostream>
#include "myClass.hpp"
int main(int argc, const char * argv[]) {
while(1){
//派生クラスのインスタンスを動的領域に確保し、抽象クラスでキャスト
myAbstract* A = new myConcreat;
delete(A);//①ここでワーニング //実は解放されていない
}
return 0;
}
また、myConcreatクラス内でデストラクタを定義しており、myConcreatクラスがデストラクタにより解体された時は"派生クラスは解放済"とメッセージが表示されるようにしています。C++では基本的に動的領域に確保したものは、それを確保したブロックの終わりに解体されるので、今回のコードではwhileループが終わるたびにmyConcreatクラスのデストラクタが働き、コンソールに、
派生クラスは解放済
と無限に表示され続けるはずです。しかし、このコードを動かすと何も表示されません。また、プログラム動作中のメモリの使用率は右肩上がりに上昇し、メモリリークが発生していることが確認されます。
今回のメモリリークへの対策
実は上記のコードは①delete(A)の部分でWarning
Delete called on 'myAbstract' that is abstract but has non-virtual
が出てきています。ざっくり訳すと、deleteはmyAbstractは抽象クラスから呼ばれるけど、抽象クラスは仮想デストラクタを持ってないよと言っています。このwarningに従って、上記のコードを修正し、myAbstractに仮想デストラクタを定義してやります(下記コード)。この仮想デストラクタは働いた時、"抽象クラスは解放済"というメッセージを出すようにしてあります。
class myAbstract{
public:
virtual void method1(void) = 0;//仮想関数の定義
//仮想デストラクタを追記
virtual ~myAbstract(){
cout<<"抽象クラスは解放済"<<endl;
}
};
このように修正した上で、問題のコードを実行すると・・・
派生クラスは解放済
抽象クラスは解放済
派生クラスは解放済
抽象クラスは解放済
・・・以下無限ループ
と言う結果になり、while(1)ループごとに動的領域に確保した派生クラスが解放され、メモリリークが解消することができました!! メモリ使用率も見てみたのですが、メモリ使用率は一定で上昇しないことが確認されました!!
今回のメモリリークの原因
とりあえず、仮想デストラクタを書いたらメモリリークを解決したけど、どうしてだろう?? この疑問の答えを解説します。答えをいうと、C++がそういう仕様だからです!!
C++では、仮想デストラクタを定義せずに今回のコードのように、
myAbstract* A = new myConcreat;
派生クラスを抽象クラスでキャストするとブロックの終わりに、myConceatクラスのデストラクタを呼びだすということを暗黙的に行ってくれません。(未定義なことは実行できない)なぜなら、そういう言語仕様だからです。
仮想デストラクタを定義すると、派生クラスをデストラクタも呼んだ後、抽象クラスのデストラクタを呼んでくれます。ただ、それだけのことなんです。それだけのことを怠るだけでメモリリークが発生するのがC++なんです。
まとめ
こんな簡単に、言語に由来したメモリリークが起こるなんて寒気がしました。C++で改修に改修を重ねる内に、メモリリークするプログラムを作り込んでしまっても何ら不思議はないと思います。仮想デストラクタ以外にもメモリリークを引き起こす要因がC++にはまだまだあるので今後調べていきたいです。