C++ 純粋仮想デストラクタ

Abdul Mateen 2023年10月12日
  1. 基本クラス ポインターを使用して派生クラス関数を呼び出す
  2. 純粋仮想関数と抽象クラス
  3. 仮想デストラクタと純粋仮想デストラクタ
  4. 純粋仮想デストラクタ
C++ 純粋仮想デストラクタ

このチュートリアルでは、C++ の純粋仮想デストラクタについて説明します。 ただし、概念を完全に理解するには、関連するさまざまな概念を理解する必要があります。

最初に、基本クラス ポインターを使用して派生クラス関数を呼び出す方法を確認し、次に継承でのデストラクタ呼び出しの問題について説明します。 最後に、純粋仮想デストラクタとその実装の問題について説明します。

基本クラス ポインターを使用して派生クラス関数を呼び出す

基本クラス ポインターには、その派生クラス オブジェクトのいずれかを指す機能があります。 これにより、さまざまな派生クラス オブジェクトを処理するために複数の派生クラス ポインターを宣言および管理する必要がなくなります。

ただし、基本クラスのポインターは、通常、派生クラスのメソッドを呼び出すことはできません。 たとえば、次のコードを参照してください。

#include <iostream>
using namespace std;
class P {};
class C : public P {
 public:
  void f() { cout << "Function C\n"; }
};
int main() {
  P *ptr = new C;
  ptr->f();
  return 0;
}

コンパイル時に、このコードは次のエラーを示します。

virtual_destructor0.cpp: In function ‘int main()’:
virtual_destructor0.cpp:12:7: error: ‘class P’ has no member named ‘f’
  ptr->f();

基本クラスで同じ関数を宣言すると、コンパイラは派生クラスの関数ではなく基本クラスの関数を呼び出します。 クラス P に関数 f を追加した次のコードを参照してください。

#include <iostream>
using namespace std;
class P {
 public:
  void f() { cout << "Function P\n"; }
};
class C : public P {
 public:
  void f() { cout << "Function C\n"; }
};
int main() {
  P *ptr = new C;
  ptr->f();
  return 0;
}

関数 f を追加した後、出力を確認します。

Function P

これは、基底クラス ポインターが派生クラス関数ではなく、基底クラス関数を呼び出すことを意味します。 クラス ポインタは、それを含むオブジェクトのクラスからではなく、そのクラスから関数を呼び出すことができるという規則を推測します。

ただし、仮想機能と呼ばれる別の重要な概念があります。 仮想関数により、派生クラスは、基本クラス ポインターが呼び出すことができる基本クラス関数をオーバーライドできます。

関数のオーバーライドとは、派生クラスが対応する基底クラスの関数に独自の機能を提供することを意味します。 基本クラスは、派生クラスでのオーバーライドを可能にするために、キーワード virtual とその関数を書き込みます。

繰り返しますが、コードを参照してください。

#include <iostream>
using namespace std;
class P {
 public:
  virtual void f() { cout << "Function P\n"; }
};
class C : public P {
 public:
  void f() { cout << "Function C\n"; }
};
int main() {
  P *ptr = new C;
  ptr->f();
  return 0;
}

基本クラス関数 f() の前に virtual キーワードが追加されていることに注意してください。 新しく追加した後、出力を確認します。

Function C

これは、基本クラス ポインターが、この基本ポインターによって参照されるオブジェクトの型に従って関数を呼び出していることを意味します (つまり、派生クラスのメンバーが実行されます)。 以前の動作は、ポインター型 (つまり、基本クラス) のメンバーを呼び出すことだったことを思い出してください。

ただし、仮想クラスの機能を提供するのは、派生クラスの実装者の選択です。これは、派生クラスが仮想基本関数をオーバーライドする場合とオーバーライドしない場合があることを意味します。

クラスのアーキテクト (設計者) は、抽象基本クラス (ABC とも呼ばれます) や純粋仮想関数などのより多くの概念を含む基本クラス関数を実装する派生クラスを強制することがあります。

それらについて簡単に説明します。 必要に応じて、参照された Web サイトにアクセスできます。

純粋仮想関数と抽象クラス

基本クラスは、純粋仮想関数を宣言して、派生クラスでの仮想基本メソッドの実装を強制できます。 純粋仮想関数には実装がありません。

また、1つ以上の純粋仮想関数を持つクラスは、抽象クラスになります。 抽象クラスはインスタンス化できません (ただし、派生クラス オブジェクトを指すポインターを宣言することはできます)。

純粋仮想関数の構文は少し異なります。

virtual void f() = 0;

純粋仮想関数には定義も本体もありません。 したがって、呼び出し可能ではありません。 ただし、純粋仮想関数を実装する派生クラスを強制します。

それ以外の場合、派生クラスは抽象になります (つまり、インスタンス化できません)。

逆に、クラスを抽象化するには、抽象関数を 1つだけ作成する必要があります。 この文については、記事の後半で参照します。

仮想デストラクタと純粋仮想デストラクタ

まず、デストラクタはオーバーライドできないことに注意してください。 ただし、基本クラスと子クラスの両方のデストラクタを呼び出すには、ポインタの (基本) クラスのデストラクタが仮想である必要があります。

それ以外の場合は、子クラスのデストラクタのみが呼び出されます。 たとえば、次のコードを見てみましょう。

#include <iostream>
using namespace std;
class P {
 public:
  ~P() { cout << "P Class Destructor\n"; }
};
class C : public P {
 public:
  ~C() { cout << "C Class Destructor\n"; }
};
int main() {
  P *ptr = new C;
  delete ptr;
  return 0;
}

出力:

P Class Destructor

ここで、ポインターはクラス P であり、オブジェクトはクラス C であることがわかります。 delete 操作は基本クラスのデストラクタのみを呼び出します。つまり、派生クラスのリソースを削除する必要がある場合、それを行うことはできません。

この目的のために、基本クラスで仮想デストラクタを宣言する必要があります。 ご存知のように、このためには、基本クラスのデストラクタに virtual キーワードを追加する必要があります。

サンプルコードは次のとおりです。

#include <iostream>
using namespace std;
class P {
 public:
  virtual ~P() { cout << "P Class Destructor\n"; }
};
class C : public P {
 public:
  ~C() { cout << "C Class Destructor\n"; }
};
int main() {
  P *ptr = new C;
  delete ptr;
  return 0;
}

出力:

C Class Destructor
P Class Destructor

純粋仮想デストラクタ

最後に、本題に移って、純粋な仮想デストラクタを介して親クラスと派生クラスの両方のデストラクタを呼び出す問題を解決しましょう。

前述のように、デストラクタは他の関数とは異なり、デストラクタをオーバーライドするという概念はありません。 したがって、純粋仮想デストラクタは、派生クラスにデストラクタの実装を強制することはできません。

では、別の角度から物事を見てみましょう。 クラスを抽象化するには、少なくとも 1つの純粋仮想関数を宣言する必要があります。

このような場合、派生クラスは、純粋仮想関数を実装して具象クラスになるようにバインドされます。 したがって、このクラスで純粋な仮想デストラクタを宣言することにより、クラスを抽象化します。これにより、派生クラスに何も実装することは強制されません。

純粋な仮想デストラクタのオッズ

No Free Lunch の定理を常に覚えておいてください。 純粋な仮想デストラクタを宣言すると、いくつかの問題が発生します。

次の例でこれらを見てみましょう。

class P {
 public:
  virtual ~P() = 0;
};

このコードをコンパイルすると、次のエラーが発生します。

/tmp/ccZfsvAh.o: In function `C::~C()':
virtual_destructor4.cpp:(.text._ZN1CD2Ev[_ZN1CD5Ev]+0x22): undefined reference to `P::~P()'
collect2: error: ld returned 1 exit status

幸いなことに、世界の複雑な問題のほとんどには、簡単な解決策があります。 このエラーは、コンパイラが利用できないデストラクタの定義を探しているために発生します。

したがって、解決策は純粋仮想デストラクタを定義することです。 矛盾しているようです。 純粋仮想関数の定義を要求するコンパイラ?

はい、これは C++ の他のオッズの 1つです。 クラス内に純粋仮想デストラクタを実装することはできません。 ただし、クラス外で実装できます。

次のコードを見てください。

class P {
 public:
  virtual ~P() = 0;
};
P::~P() { cout << "P Class Destructor\n"; }

最後の行では、クラスの外部で純粋な仮想デストラクタを定義しており、コンパイラ エラーは発生しません。 また、基本クラスと派生クラスの両方のデストラクタを呼び出すことができます。

C Class Destructor
P Class Destructor

結論として、抽象基本クラスを作成するには、その内部で純粋仮想デストラクタを宣言できます。 ただし、この純粋な仮想デストラクタをクラスの外で定義する必要があります (リソース/メモリを消去できる場所)。 このようにして、基本クラスと派生クラスの両方のデストラクタを呼び出すことができます。

関連記事 - C++ Destructor