この記事の概要
C++で基底クラスのデストラクタにvirtualを付けることの意味を理解していなかったので、実験してみて理解したことを書き残します。
結論
派生クラスのインスタンスをnewして、基底クラスのポインタで指す場合、基底クラスのポインタから派生クラスのインスタンスを完全にdeleteするために必要。
実験
こちらの記事をググってみつけて、真似して実験してみました。
https://qiita.com/ryuj/items/ad975307cd303e3bf590
実験環境
- Windows10professional
- VisualStudio2019
実験の流れ
基底クラスをB、派生クラスをDとします。コンストラクタ、デストラクタでメッセージを表示します。インスタンスの識別ができるように、コンストラクタの引数で番号を付けられるようにしました。
これらのクラスをnewする際、deleteする際に、コンストラクタ、デストラクタがどのように呼び出されるかを確認します。
実験その1
まずは、基底クラスのデストラクタに修飾子を付けないパターンでやってみます。
クラスの定義、実装はこの通り。
#include<iostream>
//===================================================
class B {
public:
B(int n=0){
n_=n;
std::cout<<"constructor in B"<<n_<<std::endl;
}
~B(){
std::cout<<"destructor in B"<<n_<<std::endl;
}
private:
int n_;
};///////////////////////////////////////////////////
//===================================================
class D : public B {
public:
D(int n=0):B(n){
n_=n;
std::cout<<"constructor in D"<<n_<<std::endl;
}
~D(){
std::cout<<"destructor in D"<<n_<<std::endl;
}
private:
int n_;
};///////////////////////////////////////////////////
では実験開始。
newとdeleteをこんな感じでやってみます。
//===================================================
int main(int argc, char* argv[])
{
std::cout << "Bのインスタンスを、B型ポインタで指す"<< std::endl;
B* b1;
b1 = new B(1);
std::cout<<"..."<<std::endl;
delete b1;
std::cout<<"-----"<<std::endl;
std::cout << "Dのインスタンスを、D型ポインタで指す" << std::endl;
D* d2;
d2 = new D(2);
std::cout<<"..."<<std::endl;
delete d2;
std::cout<<"-----"<<std::endl;
std::cout << "Dのインスタンスを、B型ポインタで指す" << std::endl;
B* b3;
b3 = new D(3);
std::cout<<"..."<<std::endl;
delete b3;
std::cout << "-----" << std::endl;
return 0;
}////////////////////////////////////////////////////
実行結果は・・・
Bのインスタンスを、B型ポインタで指す
constructor in B1
...
destructor in B1
-----
Dのインスタンスを、D型ポインタで指す
constructor in B2
constructor in D2
...
destructor in D2
destructor in B2
-----
Dのインスタンスを、B型ポインタで指す
constructor in B3
constructor in D3
...
destructor in B3
-----
「Bのインスタンスを、B型ポインタで指す」場合は、newしたらBのコンストラクタが呼ばれて、deleteしたらBのデストラクタが呼ばれます。想定通り。
「Dのインスタンスを、D型ポインタで指す」場合は、newしたらBのコンストラクタとDのコンストラクタが呼ばれて、deleteしたらDのデストラクタとBのデストラクタが呼ばれます。派生クラスなので基底クラスのコンストラクタ、デストラクタも呼ばれるわけです。
次が少し変則的。「Dのインスタンスを、B型ポインタで指す」場合です。newしたらBのコンストラクタとDのコンストラクタが呼ばれます。これは直感的に納得。でもdeleteしたら、Bのデストラクタだけが呼ばれました。Dのデストラクタは呼ばれていないから、DのインスタンスのB以外の部分が残ってしまった?Dのインスタンス部分も消したいのに。よくよく考えると、B型のポインタが指すのはBのデストラクタである、というこれまた直感的な解釈で一応納得しました。
実験その2
次に、基底クラスのデストラクタにvirtual修飾子を付けたパターンでやってみました。
クラスの定義、実装はこの通り。
#include<iostream>
//===================================================
class B {
public:
B(int n=0){
n_=n;
std::cout<<"constructor in B"<<n_<<std::endl;
}
virtual ~B(){
std::cout<<"destructor in B"<<n_<<std::endl;
}
private:
int n_;
};///////////////////////////////////////////////////
// 以下は実験1と同様なので省略
で、実験開始。mainのルーチンは実験1と同じなので省略します。
実行結果は・・・
Bのインスタンスを、B型ポインタで指す
constructor in B1
...
destructor in B1
-----
Dのインスタンスを、D型ポインタで指す
constructor in B2
constructor in D2
...
destructor in D2
destructor in B2
-----
Dのインスタンスを、B型ポインタで指す
constructor in B3
constructor in D3
...
destructor in D3
destructor in B3
-----
今度は「Dのインスタンスを、B型ポインタで指す」で、deleteしたら、DのデストラクタとBのデストラクタが呼ばれました。これで綺麗サッパリ削除できました。
ということで、「派生クラスのインスタンスをnewして、基底クラスのポインタで指す場合、基底クラスのポインタから派生クラスのインスタンスを完全にdeleteするために必要。」ということが理解できました。
あとがき
virtual修飾子の使い方、意味をはっきりと理解せずにコーディングをしていたことが今回、解りました。ガチの製品コーディングだったら怒られますね。