LoginSignup
7
6

More than 1 year has passed since last update.

C++で基底クラスのデストラクタにvirtualを付ける理由

Posted at

この記事の概要

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修飾子の使い方、意味をはっきりと理解せずにコーディングをしていたことが今回、解りました。ガチの製品コーディングだったら怒られますね。

7
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6