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


12

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

#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には何も記述しません。


4
デストラクタは、手動で記述された場合にのみ存在すると想定しています。この仮定には誤りがあります。言語は、~derived()vecのデストラクタに委任することを提供します。あるいは、unique_ptr<base> pt派生デストラクタを知っていると仮定しています。仮想メソッドがなければ、これは当てはまりません。unique_ptrには、ランタイム表現を持たないテンプレートパラメーターである削除関数が与えられる場合がありますが、この機能はこのコードには使用できません。
アモン

コードを短くするために同じ行にブレースを配置できますか?今、私はスクロールする必要があります。
laike9m

回答:


14

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

  1. 削除するオブジェクトのアドレス。
  2. ポインタのタイプ_ptr。ポインタが中unique_ptr<base>にあるので、それは_ptrbase*です。

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

それで... 実際に指し示すderviedオブジェクトを破壊する部分はどこですか?コンパイラがaを破壊していることを知らない場合、存在することはまったく知らず、破壊すべきであることは言うまでもありません。そのため、オブジェクトの半分を破壊せずにオブジェクトを破壊しました。derivedderived::vec

コンパイラーは、破棄されるものが実際にはaであると想定することはできません。結局、から派生したクラスはいくつあってもかまいません。この特定が実際に指しているタイプをどのように知るのでしょうか?base*derived*basebase*

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

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

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


0

継承

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

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

関数のバインド

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

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

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

あなたが呼び出された場合、あなたの例から考えると、initialize_vector()コンパイラをあなたが本当にで見つかった実装を呼び出すためのものかどうかを判断しなければならないBase、または実装がで見つかりましたDerived。これを決定するには2つの方法があります。

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

この時点でコンパイラーは混乱しており、両方のオプションは等しく有効です。これがvirtualミックスに入ったときです。このキーワードが存在する場合、コンパイラはオプション2を選択し、コードが実際の値で実行されるまで、可能なすべての実装間の決定を遅らせます。このキーワードがない場合、コンパイラはオプション1を選択します。これは、それ以外の点では通常の動作であるためです。

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

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

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

より直感的に、コンパイラはDerivedand のコンストラクタの同一の実装をどのように選択しDerived2ますか?これは非常に簡単ですが、できません。コンパイラーが実際に意図したことを学習できる既存の値はありません。それはコンストラクタの仕事であるため、既存の値はありません。

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

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.