
C++入門|仮想デストラクタ
親クラス型で子クラスを扱うなら、退場処理まで子クラスに届けよう。仮想デストラクタは、継承したオブジェクトを安全に片付けるための大切な守り札です。
C++では、親クラス型のポインタを使って、子クラスのインスタンスを指すことができます。
たとえば、基本戦士を表す親クラスがあり、その子クラスとして亀仙流戦士を作ったとします。
BaseWarrior* pWarrior = new KameWarrior();このように書くと、ポインタの型はBaseWarriorですが、実際に生成されているインスタンスはKameWarriorです。
これは、ポリモーフィズムを使うときによく出てくる形です。
親クラス型のポインタで、いろいろな子クラスのインスタンスをまとめて扱えるので、とても便利です。
ただし、ここで大切になるのがデストラクタです。
newで生成したインスタンスは、使い終わったらdeleteで消去します。
delete pWarrior;このとき、pWarriorの型はBaseWarrior*です。
でも、実際のインスタンスはKameWarriorです。
では、deleteしたときに、BaseWarriorのデストラクタだけが呼ばれるのでしょうか。
それとも、KameWarriorのデストラクタも正しく呼ばれるのでしょうか。
この問題を安全に解決するために使うのが、仮想デストラクタです。
親クラスのデストラクタにvirtualを付けると、親クラス型のポインタからdeleteしても、実際の子クラスのデストラクタが先に呼ばれ、そのあと親クラスのデストラクタが呼ばれます。
ドラゴンボール風にたとえるなら、KameWarriorが特別な修行装備を身につけていた場合、退場するときにはまずその装備を外し、そのあと基本戦士としての共通処理を終える必要があります。
親クラスのデストラクタがvirtualでないと、子クラス側の装備解除が正しく行われない可能性があります。
| 状態 | delete時の動き | イメージ |
|---|---|---|
| 親クラスのデストラクタにvirtualなし | 子クラス側のデストラクタが正しく呼ばれない危険がある | 特別装備の解除が飛ばされる |
| 親クラスのデストラクタにvirtualあり | 子クラス → 親クラスの順にデストラクタが呼ばれる | 特別装備を外してから共通処理を終える |
継承して使われる可能性のある親クラスでは、デストラクタにvirtualを付けることがとても大切です。
図:親クラス型ポインタで子クラスをdeleteする場面

この図が示していること
この図では、BaseWarrior型のポインタが、実際にはKameWarriorのインスタンスを指している様子を表しています。
この状態でdelete pWarriorを行うとき、KameWarrior側のデストラクタまで正しく呼ぶ必要があります。
親クラスのデストラクタにvirtualが付いていれば、子クラスのデストラクタが先に呼ばれ、そのあと親クラスのデストラクタが呼ばれます。
仮想デストラクタとは
仮想デストラクタとは、virtual修飾子を付けたデストラクタのことです。
virtual ~BaseWarrior();通常の仮想関数と同じように、デストラクタにもvirtualを付けられます。
親クラスのデストラクタにvirtualを付けると、親クラス型のポインタで子クラスのインスタンスをdeleteした場合でも、実際の子クラスに合わせてデストラクタが呼び出されます。
たとえば、次のような形です。
BaseWarrior* pWarrior = new KameWarrior();
delete pWarrior;このとき、BaseWarriorのデストラクタがvirtualなら、次の順番でデストラクタが呼ばれます。
KameWarriorのデストラクタ
BaseWarriorのデストラクタこれは、子クラスの部分を先に片付け、そのあと親クラスの部分を片付けるという自然な流れです。
ドラゴンボール風に言えば、亀仙流戦士としての専用装備や技のエネルギーを先に解除し、そのあと基本戦士としての共通状態を終了するイメージです。
仮想デストラクタなしのクラス
まずは、親クラスのデストラクタにvirtualを付けない例を見てみます。
この例では、BaseWarriorPlainという親クラスと、それを継承したKameWarriorPlainという子クラスを作ります。
BaseWarriorPlainのデストラクタにはvirtualを付けません。
プロジェクト/ファイル名: Chap5_08/base_warrior_plain.h
#ifndef _BASE_WARRIOR_PLAIN_H_
#define _BASE_WARRIOR_PLAIN_H_
#include <iostream>
class BaseWarriorPlain {
public:
// コンストラクタ
BaseWarriorPlain() {
std::cout << "BaseWarriorPlainのコンストラクタ" << std::endl;
}
// virtualなしのデストラクタ
~BaseWarriorPlain() {
std::cout << "BaseWarriorPlainのデストラクタ" << std::endl;
}
};
#endif // _BASE_WARRIOR_PLAIN_H_このクラスでは、コンストラクタとデストラクタの中でメッセージを表示しています。
注目するのは、デストラクタです。
~BaseWarriorPlain() {
std::cout << "BaseWarriorPlainのデストラクタ" << std::endl;
}virtualが付いていません。
つまり、BaseWarriorPlain型のポインタで子クラスをdeleteしたとき、子クラス側のデストラクタ呼び出しが保証されません。
仮想デストラクタありのクラス
次に、デストラクタにvirtualを付けた親クラスを作ります。
プロジェクト/ファイル名: Chap5_08/base_warrior_virtual.h
#ifndef _BASE_WARRIOR_VIRTUAL_H_
#define _BASE_WARRIOR_VIRTUAL_H_
#include <iostream>
class BaseWarriorVirtual {
public:
// コンストラクタ
BaseWarriorVirtual() {
std::cout << "BaseWarriorVirtualのコンストラクタ" << std::endl;
}
// virtualありのデストラクタ
virtual ~BaseWarriorVirtual() {
std::cout << "BaseWarriorVirtualのデストラクタ" << std::endl;
}
};
#endif // _BASE_WARRIOR_VIRTUAL_H_こちらのデストラクタには、virtualが付いています。
virtual ~BaseWarriorVirtual() {
std::cout << "BaseWarriorVirtualのデストラクタ" << std::endl;
}このようにしておくと、BaseWarriorVirtual型のポインタで子クラスのインスタンスをdeleteした場合でも、子クラス側のデストラクタが正しく呼ばれます。
virtualなしの子クラス
次に、BaseWarriorPlainを継承したKameWarriorPlainを作ります。
プロジェクト/ファイル名: Chap5_08/kame_warrior_plain.h
#ifndef _KAME_WARRIOR_PLAIN_H_
#define _KAME_WARRIOR_PLAIN_H_
#include "base_warrior_plain.h"
#include <iostream>
class KameWarriorPlain : public BaseWarriorPlain {
public:
// コンストラクタ
KameWarriorPlain() {
std::cout << "KameWarriorPlainのコンストラクタ" << std::endl;
}
// デストラクタ
~KameWarriorPlain() {
std::cout << "KameWarriorPlainのデストラクタ" << std::endl;
}
};
#endif // _KAME_WARRIOR_PLAIN_H_KameWarriorPlainは、BaseWarriorPlainを継承しています。
class KameWarriorPlain : public BaseWarriorPlainコンストラクタでは、KameWarriorPlainの生成メッセージを表示します。
デストラクタでは、KameWarriorPlainの消去メッセージを表示します。
~KameWarriorPlain() {
std::cout << "KameWarriorPlainのデストラクタ" << std::endl;
}しかし、親クラスBaseWarriorPlainのデストラクタにvirtualが付いていないため、BaseWarriorPlain型のポインタからdeleteすると、KameWarriorPlainのデストラクタが呼ばれない危険があります。
virtualありの子クラス
次に、BaseWarriorVirtualを継承したKameWarriorVirtualを作ります。
プロジェクト/ファイル名: Chap5_08/kame_warrior_virtual.h
#ifndef _KAME_WARRIOR_VIRTUAL_H_
#define _KAME_WARRIOR_VIRTUAL_H_
#include "base_warrior_virtual.h"
#include <iostream>
class KameWarriorVirtual : public BaseWarriorVirtual {
public:
// コンストラクタ
KameWarriorVirtual() {
std::cout << "KameWarriorVirtualのコンストラクタ" << std::endl;
}
// デストラクタ
~KameWarriorVirtual() {
std::cout << "KameWarriorVirtualのデストラクタ" << std::endl;
}
};
#endif // _KAME_WARRIOR_VIRTUAL_H_KameWarriorVirtualは、BaseWarriorVirtualを継承しています。
BaseWarriorVirtualのデストラクタにはvirtualが付いているため、BaseWarriorVirtual型のポインタからdeleteしても、KameWarriorVirtualのデストラクタが先に呼ばれます。
そのあと、BaseWarriorVirtualのデストラクタが呼ばれます。
main.cppで動きを確認する
最後に、main.cppを作ります。
ここでは、virtualなしの場合と、virtualありの場合を並べて確認します。
プロジェクト/ファイル名: Chap5_08/main.cpp
#include <iostream>
#include "base_warrior_plain.h"
#include "base_warrior_virtual.h"
#include "kame_warrior_plain.h"
#include "kame_warrior_virtual.h"
using namespace std;
int main(int argc, char** argv) {
cout << "*** 仮想デストラクタなし ***" << endl;
// 親クラス型のポインタで子クラスのインスタンスを指す
BaseWarriorPlain* pPlain = new KameWarriorPlain();
// 親クラス型のポインタからdeleteする
// 親クラスのデストラクタにvirtualがないため、子クラス側の後片付けが保証されない
delete pPlain;
cout << "*** 仮想デストラクタあり ***" << endl;
// 親クラス型のポインタで子クラスのインスタンスを指す
BaseWarriorVirtual* pVirtual = new KameWarriorVirtual();
// 親クラス型のポインタからdeleteする
// 親クラスのデストラクタにvirtualがあるため、子クラス側から順に後片付けされる
delete pVirtual;
return 0;
}実行結果の例
*** 仮想デストラクタなし ***
BaseWarriorPlainのコンストラクタ
KameWarriorPlainのコンストラクタ
BaseWarriorPlainのデストラクタ
*** 仮想デストラクタあり ***
BaseWarriorVirtualのコンストラクタ
KameWarriorVirtualのコンストラクタ
KameWarriorVirtualのデストラクタ
BaseWarriorVirtualのデストラクタ仮想デストラクタなしの部分では、KameWarriorPlainのコンストラクタは呼ばれています。
しかし、delete時にはBaseWarriorPlainのデストラクタだけが表示されています。
一方、仮想デストラクタありの部分では、delete時にKameWarriorVirtualのデストラクタが先に呼ばれ、そのあとBaseWarriorVirtualのデストラクタが呼ばれています。
C++の厳密なルールでは、親クラスのデストラクタがvirtualでない状態で、親クラス型ポインタから子クラスのインスタンスをdeleteするのは安全ではありません。
そのため、継承して使う親クラスでは、デストラクタをvirtualにするのが基本です。
図:virtualなしとvirtualありのdelete時の違い

この図が示していること
この図では、virtualなしとvirtualありで、delete時のデストラクタ呼び出しがどう変わるかを表しています。
virtualなしでは、親クラス型ポインタからdeleteしたときに、子クラス側のデストラクタが正しく呼ばれない危険があります。
virtualありでは、実際のインスタンスが子クラスであることに合わせて、子クラスのデストラクタが先に呼ばれ、そのあと親クラスのデストラクタが呼ばれます。
生成時のコンストラクタの順番
まず、newしたときの動きを確認しましょう。
BaseWarriorVirtual* pVirtual = new KameWarriorVirtual();このように、子クラスのインスタンスを生成すると、コンストラクタは親クラスから子クラスの順番で呼ばれます。
BaseWarriorVirtualのコンストラクタ
KameWarriorVirtualのコンストラクタこれは、子クラスの中には親クラス部分が含まれているからです。
KameWarriorVirtualという戦士を作るには、まずBaseWarriorVirtualとしての基本部分を作り、そのあとKameWarriorVirtualとしての専用部分を作ります。
| タイミング | 呼び出し順 |
|---|---|
| 生成時 | 親クラスのコンストラクタ → 子クラスのコンストラクタ |
| 消去時 | 子クラスのデストラクタ → 親クラスのデストラクタ |
生成時は親から子へ。
消去時は子から親へ。
この流れを押さえると、仮想デストラクタの意味が分かりやすくなります。
消去時のデストラクタの順番
virtualありの場合、delete時には次の順番になります。
KameWarriorVirtualのデストラクタ
BaseWarriorVirtualのデストラクタこれは、作るときと逆の順番です。
ドラゴンボール風に言えば、亀仙流戦士としての専用装備や気の設定を先に解除し、そのあと基本戦士としての共通部分を終了するイメージです。
もし子クラス側のデストラクタが呼ばれないと、専用装備の解除処理が行われません。
プログラムの世界では、それがメモリ解放漏れやリソース解放漏れにつながる可能性があります。
virtualなしが危険な理由
virtualなしの場合、次のような形が危険です。
BaseWarriorPlain* pPlain = new KameWarriorPlain();
delete pPlain;pPlainの型はBaseWarriorPlain*です。
しかし、実際に生成されているのはKameWarriorPlainです。
親クラスのデストラクタにvirtualが付いていないと、delete時に子クラスのデストラクタが正しく呼ばれることが保証されません。
そのため、KameWarriorPlain側で大切な後片付けをしていた場合、その処理が実行されない危険があります。
たとえば、子クラス側で次のようなものを管理している場合です。
| 子クラスが持つもの | デストラクタで必要な処理 |
|---|---|
| 動的確保したメモリ | deleteやdelete[]で解放する |
| ファイル | closeする |
| 通信接続 | 接続を閉じる |
| 修行装備の管理情報 | 終了処理を行う |
子クラスのデストラクタが呼ばれないと、これらの後片付けが抜けてしまう可能性があります。
C++では、このような状態は非常に危険です。
virtualありなら子クラス側から安全に片付く
virtualありの場合は、次のように書いても安全に片付けられます。
BaseWarriorVirtual* pVirtual = new KameWarriorVirtual();
delete pVirtual;BaseWarriorVirtualのデストラクタにはvirtualが付いています。
virtual ~BaseWarriorVirtual() {
std::cout << "BaseWarriorVirtualのデストラクタ" << std::endl;
}そのため、delete pVirtualを実行すると、実際のインスタンスであるKameWarriorVirtualのデストラクタが呼ばれます。
そのあと、BaseWarriorVirtualのデストラクタが呼ばれます。
つまり、次の順番になります。
KameWarriorVirtualのデストラクタ
BaseWarriorVirtualのデストラクタこれなら、子クラスが持つ専用の後片付けを先に行い、そのあと親クラスの共通部分を片付けられます。
親クラス側にvirtualを付けることが大切
仮想デストラクタで大切なのは、親クラスのデストラクタにvirtualを付けることです。
子クラス側のデストラクタにだけ気をつけるのではありません。
親クラス型のポインタからdeleteする可能性があるなら、親クラスのデストラクタをvirtualにします。
class BaseWarrior {
public:
virtual ~BaseWarrior();
};このようにしておくと、BaseWarrior*で子クラスを指している場合でも、delete時に正しいデストラクタの連鎖が起きます。
| クラス | デストラクタの書き方 | 安全性 |
|---|---|---|
| 親クラス | ~BaseWarrior(); | 親クラス型ポインタでdeleteすると危険 |
| 親クラス | virtual ~BaseWarrior(); | 子クラス側まで正しく片付けられる |
| 子クラス | ~KameWarrior(); | 親がvirtualなら、このデストラクタも適切に呼ばれる |
親クラスがvirtualにしておくことで、派生先のクラスも安全に扱いやすくなります。
仮想デストラクタが必要になる典型例
仮想デストラクタが特に大切になるのは、親クラス型のポインタや参照で子クラスを扱う場面です。
BaseWarrior* warriors[2];
warriors[0] = new KameWarriorVirtual();
warriors[1] = new KameWarriorVirtual();このように、配列やリストで複数の子クラスを親クラス型としてまとめることがあります。
そのあと、次のようにdeleteすることもあります。
for (int i = 0; i < 2; i++) {
delete warriors[i];
}このとき、BaseWarriorのデストラクタがvirtualでないと、子クラス側のデストラクタが呼ばれない危険があります。
ポリモーフィズムを使う設計では、親クラス型ポインタで子クラスをdeleteする場面が自然に出てきます。
だからこそ、継承される親クラスのデストラクタにはvirtualを付けます。
図:子クラスの後片付けが飛ばされると危険

この図が示していること
この図では、子クラス側のデストラクタが呼ばれないと、子クラス専用の後片付けが飛ばされる危険があることを表しています。
virtualなしでは、親クラス型ポインタからdeleteしたときに、子クラスの片付けが正しく行われない可能性があります。
virtualありでは、子クラスのデストラクタが先に呼ばれ、そのあと親クラスのデストラクタが呼ばれるため、安全に後片付けできます。
仮想関数と仮想デストラクタの関係
virtualは、通常のメンバ関数だけでなく、デストラクタにも付けられます。
通常の仮想関数は、親クラス型ポインタから呼び出したときに、実際の子クラスの関数を呼び出すために使います。
仮想デストラクタも考え方は似ています。
親クラス型ポインタからdeleteしたときに、実際の子クラスのデストラクタを呼び出すために使います。
| 種類 | 目的 |
|---|---|
| virtual void shout(); | 親クラス型ポインタから子クラス側の関数を呼ぶ |
| virtual ~BaseWarrior(); | 親クラス型ポインタから子クラス側のデストラクタを呼ぶ |
どちらも、「ポインタの型」ではなく「実際のインスタンス」に合わせて処理を行うための仕組みです。
デストラクタは消去時の最後の安全装置
デストラクタは、インスタンスが消えるときに自動的に呼び出される関数です。
子クラスのデストラクタには、子クラス専用の後片付けを書くことがあります。
たとえば、KameWarriorが専用の修行データを動的に確保している場合、その解放処理はKameWarriorのデストラクタに書くことになります。
もしKameWarriorのデストラクタが呼ばれなければ、その解放処理も実行されません。
そのため、継承を使う設計では、デストラクタをただの終了メッセージとして考えないことが大切です。
デストラクタは、メモリやリソースを安全に片付けるための最後の安全装置です。
仮想デストラクタで押さえたいポイント
仮想デストラクタでは、次のポイントを押さえておきましょう。
| ポイント | 内容 |
|---|---|
| 仮想デストラクタ | virtualを付けたデストラクタ |
| 必要な場面 | 親クラス型ポインタで子クラスをdeleteする場面 |
| virtualなし | 子クラスのデストラクタが正しく呼ばれない危険がある |
| virtualあり | 子クラス → 親クラスの順にデストラクタが呼ばれる |
| 生成時の順番 | 親クラスのコンストラクタ → 子クラスのコンストラクタ |
| 消去時の順番 | 子クラスのデストラクタ → 親クラスのデストラクタ |
| 基本方針 | 継承して使う親クラスのデストラクタにはvirtualを付ける |
C++では、親クラス型のポインタで子クラスのインスタンスを扱うことがよくあります。
そのとき、deleteまで安全に行うには、親クラスのデストラクタをvirtualにしておく必要があります。
ドラゴンボール風に言えば、BaseWarrior型の司令端末からKameWarriorを退場させるときでも、亀仙流戦士としての専用装備や気の設定をきちんと解除してから、基本戦士としての共通処理を終える必要があります。
その順番を守らせるのが、仮想デストラクタです。
継承される可能性のある親クラスでは、次の形を基本として覚えておきましょう。
virtual ~BaseWarrior();この1行があることで、ポリモーフィズムを使ったクラス設計を安全に扱いやすくなります。
