デストラクター (C++)

デストラクターは、オブジェクトがスコープ外になった場合、または呼び出しによって明示的に破棄されたときに自動的に呼び出deletedelete[]されるメンバー関数です。 デストラクターの名前はクラスと同じで、その前にチルダ (~) が付いています。 たとえば、クラス String のデストラクターを宣言するには、~String() とします。

デストラクターを定義しない場合、コンパイラは既定のデストラクターを提供し、一部のクラスではこれで十分です。 クラスメイン明示的に解放する必要があるリソース (システム リソースへのハンドルや、クラスのインスタンスが破棄されたときに解放する必要があるメモリへのポインターなど) が含まれている場合は、カスタム デストラクターを定義する必要があります。

String クラスの次のような宣言があるとします。

// spec1_destructors.cpp
#include <string> // strlen()

class String
{
    public:
        String(const char* ch);  // Declare the constructor
        ~String();               // Declare the destructor
    private:
        char* _text{nullptr};
};

// Define the constructor
String::String(const char* ch)
{
    size_t sizeOfText = strlen(ch) + 1; // +1 to account for trailing NULL

    // Dynamically allocate the correct amount of memory.
    _text = new char[sizeOfText];

    // If the allocation succeeds, copy the initialization string.
    if (_text)
    {
        strcpy_s(_text, sizeOfText, ch);
    }
}

// Define the destructor.
String::~String()
{
    // Deallocate the memory that was previously reserved for the string.
    delete[] _text;
}

int main()
{
    String str("We love C++");
}

上の例では、デストラクター String::~Stringdelete[] 演算子を使用して、テキストの保存のために動的に割り当てられた領域を解放します。

デストラクターの宣言

デストラクターはクラスと同じ名前の関数ですが、先頭にティルダ (~) が付きます。

デストラクターの宣言には以下の規則が適用されます 。

  • 引数を受け入れないでください。
  • 値 (または void) を返さないでください。
  • 、、またはstaticとしてconstvolatile宣言することはできません。 ただし、constvolatile、または static として宣言されているオブジェクトを破棄するために呼び出すことができます。
  • virtual として宣言する必要があります。 仮想デストラクターを使用すると、オブジェクトの型を知らなくてもオブジェクトを破棄できます。オブジェクトの正しいデストラクターは、仮想関数メカニズムを使用して呼び出されます。 デストラクターは、抽象クラスの純粋仮想関数として宣言することもできます。

デストラクターの使用

デストラクターは、次のいずれかのイベントが発生したときに呼び出されます。

  • ブロック スコープを持つローカル (自動) オブジェクトがスコープから外れます。
  • を使用して割り当てられたオブジェクトの割り当てを解除するために使用 delete します new。 未定義の動作で結果を使用 delete[] します。
  • を使用して割り当てられたオブジェクトの割り当てを解除するために使用 delete[] します new[]。 未定義の動作で結果を使用 delete します。
  • 一時オブジェクトの有効期間は終了します。
  • プログラムは終了し、グローバルまたはスタティック オブジェクトが存在します。
  • デストラクターは、デストラクター関数の完全修飾名を使用して明示的に呼び出されます

デストラクターは、自由にクラス メンバー関数を呼び出したり、クラス メンバーのデータにアクセスしたりできます。

デストラクターの使用には 2 つの制限があります:

  • アドレスを取得することはできません。

  • 派生クラスは、基底クラスのデストラクターを継承しません。

破棄の順序

オブジェクトがスコープ外になるとき、または削除されるとき、完全な破棄のイベントの順序は次のとおりです。

  1. クラスのデストラクターが呼び出され、デストラクター関数の本体が実行されます。

  2. 非静的メンバー オブジェクトのデストラクターは、クラス宣言での出現順序の逆順で呼び出されます。 これらのメンバーの構築に使用されるオプションのメンバー初期化リストは、構築または破棄の順序には影響しません。

  3. 非仮想基底クラスのデストラクターは、宣言の逆順で呼び出されます。

  4. 仮想基底クラスのデストラクターは、宣言の逆順で呼び出されます。

// order_of_destruction.cpp
#include <cstdio>

struct A1      { virtual ~A1() { printf("A1 dtor\n"); } };
struct A2 : A1 { virtual ~A2() { printf("A2 dtor\n"); } };
struct A3 : A2 { virtual ~A3() { printf("A3 dtor\n"); } };

struct B1      { ~B1() { printf("B1 dtor\n"); } };
struct B2 : B1 { ~B2() { printf("B2 dtor\n"); } };
struct B3 : B2 { ~B3() { printf("B3 dtor\n"); } };

int main() {
   A1 * a = new A3;
   delete a;
   printf("\n");

   B1 * b = new B3;
   delete b;
   printf("\n");

   B3 * b2 = new B3;
   delete b2;
}
A3 dtor
A2 dtor
A1 dtor

B1 dtor

B3 dtor
B2 dtor
B1 dtor

仮想基底クラス

仮想基底クラスのデストラクターが有向非循環グラフ (深さ優先、左から右、後順走査) で外観の逆の順序で呼び出されます。 次の図は、継承グラフを示しています。

Inheritance graph that shows virtual base classes.

A から E というラベルが付いた 5 つのクラスが継承グラフに配置されます。 クラス E は B、C、D の基底クラスです。クラス C と D は A と B の基底クラスです。

次に、図に示すクラスのクラス定義を示します。

class A {};
class B {};
class C : virtual public A, virtual public B {};
class D : virtual public A, virtual public B {};
class E : public C, public D, virtual public B {};

コンパイラは、E 型のオブジェクトの仮想基底クラスの破棄の順序を決定するため、次のアルゴリズムを適用してリストを作成します。

  1. グラフを左に走査し、グラフの最も深いポイント (この場合は E) で開始します。
  2. すべてのノードが表示されるまで左方向への走査を実行します。 現在のノードの名前を確認します。
  3. 保持するノードが仮想基底クラスであるかどうかを確認するために、前のノード (右下隅) を再表示します。
  4. 保持するノードが仮想基底クラスである場合は、既に構成されているかどうかを参照するために、リストを確認します。 仮想基底クラスでない場合は無視します。
  5. 記憶されたノードがまだ一覧にない場合は、一覧の一番下に追加します。
  6. 上方向および次のパスに沿って右へグラフを走査します。
  7. 手順 2 に進みます。
  8. 最後の上方向のパスが使い果たされたら、現在のノードの名前を確認します。
  9. 手順 3. に進みます。
  10. 下部のノードが再び現在のノードになるまで、このプロセスを続行します。

したがって、クラス E の場合、破棄の順序は次のようになります。

  1. 非仮想基底クラス E
  2. 非仮想基底クラス D
  3. 非仮想基底クラス C
  4. 仮想基底クラス B
  5. 仮想基底クラス A

このプロセスにより、一意のエントリの順序付きリストが作成されます。 いずれのクラス名も、2 回表示されることはありません。 リストが構築されると、逆の順序でウォークされ、リスト内の各クラスのデストラクターが最後から最初のクラスまで呼び出されます。

構築または破棄の順序は、主に、1 つのクラスのコンストラクターまたはデストラクターが最初に作成される他のコンポーネントに依存する場合や、(前に示した図の) デストラクター A がコードの実行時にまだ存在することに依存している B 場合、またはその逆の場合に重要です。

継承グラフでのクラス間のこのような依存関係は、後で派生するクラスが左端のパスを変更し、それによって構築と破棄の順序を変更できるため、本質的に危険です。

非仮想基底クラス

非仮想基底クラスのデストラクターは、基底クラスの名前を宣言した順序と逆の順序で呼び出されます。 クラス宣言の例を次に示します。

class MultInherit : public Base1, public Base2
...

前の例では、Base2 のデストラクターは、Base1 のデストラクターの前に呼び出されます。

明示的なデストラクター呼び出し

デストラクターを明示的に呼び出す必要はほとんどありません。 ただし、絶対アドレスにあるオブジェクトのクリーンアップを実行すると便利な場合があります。 これらのオブジェクトは、一般に、配置引数を受け取るユーザー定義の new 演算子を使用して割り当てられます。 delete空きストアから割り当てられていないため、このメモリの割り当てを解除できません (詳細については、「New 演算子と delete 演算子」を参照してください)。 ただし、デストラクターへの呼び出しは適切なクリーンアップを実行できます。 オブジェクトのデストラクターを明示的に呼び出すには、s (String クラス) で次のいずれかのステートメントを使用します。

s.String::~String();     // non-virtual call
ps->String::~String();   // non-virtual call

s.~String();       // Virtual call
ps->~String();     // Virtual call

先に示したデストラクターへの明示的な呼び出しの表記は、型がデストラクターを定義するかどうかにかかわらず使用できます。 これにより、デストラクターが型に対して定義されているかどうかを確認せずにこのような明示的な呼び出しを行うことができます。 何も定義されていないデストラクターへの明示的な呼び出しは無効です。

信頼性の高いプログラミング

クラスは、リソースを取得する場合にデストラクターを必要とし、リソースを安全に管理するために、コピー コンストラクターとコピー割り当てを実装する必要がある場合があります。

これらの特殊関数がユーザーによって定義されていない場合、コンパイラによって暗黙的に定義されます。 暗黙的に生成されたコンストラクターと代入演算子は、浅い、メンバーごとのコピーを実行します。これは、オブジェクトがリソースを管理している場合は、ほぼ確実に問題があります。

次の例では、暗黙的に生成されたコピー コンストラクターがポインター str1.text を作成し、str2.text が同じメモリを参照します。copy_strings() から戻ると、そのメモリは 2 回削除されます。これは未定義の動作です。

void copy_strings()
{
   String str1("I have a sense of impending disaster...");
   String str2 = str1; // str1.text and str2.text now refer to the same object
} // delete[] _text; deallocates the same memory twice
  // undefined behavior

デストラクター、コピー コンストラクター、またはコピー代入演算子を明示的に定義すると、移動コンストラクターと移動代入演算子の暗黙的な定義が禁止されます。 この場合、コピー操作の実行に失敗した場合、通常、コピーに負荷がかかると、最適化の機会が失われます。

関連項目

コピー コンストラクターとコピー代入演算子
移動コンストラクターと移動代入演算子