web-dev-qa-db-ja.com

派生クラスが生の動的メモリを割り当てない場合、基本クラスに仮想デストラクタが必要なのはなぜですか?

次のコードはメモリリークを引き起こします。

#include <iostream>
#include <memory>
#include <vector>

using namespace std;

class base
{
    void virtual initialize_vector() = 0;
};

class derived : public base
{
private:
    vector<int> vec;

public:
    derived()
    {
        initialize_vector();
    }

    void initialize_vector()
    {
        for (int i = 0; i < 1000000; i++)
        {
            vec.Push_back(i);
        }
    }
};

int main()
{
    for (int i = 0; i < 100000; i++)
    {
        unique_ptr<base> pt = make_unique<derived>();
    }
}

クラス派生は生の動的メモリを割り当てず、unique_ptrはそれ自体の割り当てを解除するため、私にはあまり意味がありませんでした。クラスベースの暗黙のデストラクタが派生クラスの代わりに呼び出されていることがわかりますが、ここでそれが問題である理由はわかりません。派生の明示的なデストラクタを作成する場合、vecには何も記述しません。

12

コンパイラが_delete _ptr;_のデストラクタ内で暗黙の_unique_ptr_を実行しようとする場合(__ptr_は_unique_ptr_に格納されているポインタです)、2つのことを正確に認識します。

  1. 削除するオブジェクトのアドレス。
  2. __ptr_が指すポインタのタイプ。ポインターは_unique_ptr<base>_にあるため、__ptr_のタイプは_base*_です。

これはコンパイラが知っているすべてです。したがって、base型のオブジェクトを削除する場合、~base()を呼び出します。

それで... 実際にが指すderviedオブジェクトを破棄する部分はどこですか?コンパイラがderivedを破棄していることを認識していない場合、_derived::vec_ 存在するを認識していないため、破棄する必要があることは言うまでもありません。つまり、オブジェクトの半分を破壊せずに残してオブジェクトを破壊しました。

コンパイラはassumeできません。破棄される_base*_は実際には_derived*_です。結局のところ、baseから派生したクラスはいくつでも存在する可能性があります。この特定の_base*_が実際に指す型をどのようにして知るのでしょうか?

コンパイラーがしなければならないことは、正しいデストラクタを呼び出すことです(そうです、derivedにはデストラクタがあります。_= delete_デストラクタがない限り、every classにデストラクタがあるかどうかしない)。これを行うには、baseに格納されている情報を使用して、呼び出すデストラクタコードの正しいアドレスを取得する必要があります。この情報は、実際のクラスのコンストラクタによって設定されます。次に、この情報を使用して、_base*_を、対応するderivedクラスのアドレスへのポインターに変換する必要があります(別のアドレスにある場合とない場合があります。そうです)。そして、そのデストラクタを呼び出すことができます。

今説明したそのメカニズムは?これは一般に「仮想ディスパッチ」と呼ばれます。つまり、基本クラスへのポインタ/参照があるときにvirtualとマークされた関数を呼び出すと必ず発生します。

派生クラス関数を、基本クラスのポインター/参照だけで呼び出したい場合は、その関数をvirtualとして宣言する必要があります。デストラクタは基本的にこの点で違いはありません。

14
Nicol Bolas

継承

継承の全体のポイントは、多くの異なる実装間で共通のインターフェイスとプロトコルを共有することです。これにより、派生クラスのインスタンスを、他の派生型の他のインスタンスと同じように扱うことができます。

C++の継承では、実装の詳細も伴います。デストラクタを仮想としてマークする(またはマークしない)ことは、そのような実装の詳細の1つです。

関数バインディング

関数、またはコンストラクタやデストラクタなどの特殊なケースが呼び出されると、コンパイラはどの関数の実装を意図しているかを選択する必要があります。次に、この意図に従ってマシンコードを生成する必要があります。

これを機能させる最も簡単な方法は、コンパイル時に関数を選択し、値に関係なくそのコードが実行されたときに常に関数のコードが実行されるように、十分なマシンコードを発行することです。これは継承を除いてうまくいきます。

関数(コンストラクタやデストラクタを含む任意の関数)を持つ基本クラスがあり、コードがその関数を呼び出す場合、これはどういう意味ですか?

あなたの例から言うと、initialize_vector()を呼び出した場合、コンパイラは、本当にBaseにある実装を呼び出すのか、Derivedにある実装を呼び出すのかを決定する必要があります。これを決定する方法は2つあります。

  1. 1つ目は、Baseタイプから呼び出したため、Baseでの実装を意味することを決定することです。
  2. 2つ目は、Base型の値に格納されている値のランタイムタイプがBaseまたはDerivedである可能性があるため、どの呼び出しを行うかについての決定が、呼び出されたとき(呼び出されるたびに)、実行時に作成する必要があります。

この時点でコンパイラは混乱しています。どちらのオプションも同じように有効です。これがvirtualが登場する時期です。このキーワードが存在する場合、コンパイラーはオプション2を選択し、コードが実際の値で実行されるまで、可能なすべての実装間の決定を遅らせます。このキーワードがない場合、コンパイラーはオプション1を選択します。これは、それ以外の場合は通常の動作だからです。

仮想関数呼び出しの場合、コンパイラーはオプション1を選択する場合があります。ただし、これが常に当てはまることを証明できる場合に限ります。

コンストラクタとデストラクタ

それでは、なぜ仮想コンストラクタを指定しないのでしょうか。

より直感的に、コンパイラーはDerivedと_Derived2_のコンストラクターの同じ実装間でどのように選択しますか?これはかなり単純ですが、できません。コンパイラーが実際に意図したものを学習できる既存の値はありません。これはコンストラクターの仕事なので、既存の値はありません。

では、なぜ仮想デストラクタを指定する必要があるのでしょうか。

より直感的に、コンパイラはBaseDerivedの実装をどのように選択するでしょうか?これらは単なる関数呼び出しなので、関数呼び出しの動作が発生します。宣言された仮想デストラクタがない場合、コンパイラは、ランタイムタイプの値に関係なく、Baseデストラクタに直接バインドすることを決定します。

多くのコンパイラでは、派生がデータメンバーを宣言していない場合、または他の型から継承していない場合、~Base()の動作は適切ですが、保証されていません。それは、まだ点火されていない火炎放射器の前に立っているように、偶然に純粋に機能します。しばらく元気です。

C++で基本型またはインターフェイス型を宣言する唯一の正しい方法は、仮想デストラクタを宣言することです。これにより、その型の型階層の特定のインスタンスに対して正しいデストラクタが呼び出されます。これにより、インスタンスを最もよく知っている関数が、そのインスタンスを正しくクリーンアップできます。

0
Kain0_0