【電子工作番外編】Z-0.C++番外編 Baseクラスのデストラクタのvirtual記載必要性
こんばんわ。
SAIです。
C++を有効に活用するには、継承をうまく使う必要があります。
この継承ですが、うまく使えばとっても良い機能なのですが、
しっかり理解せずにベースクラスを設計すると、解放漏れや意図しない呼び出しにつながります。
この継承について、よくわからないという疑問を持った方がいたので、
実験とあわせて解説します。
今回は、ベースクラスのデストラクタに virtual を付ける必要はあるのか?
ベースクラスのコンストラクタ、デストラクタの呼び出される順と合わせて
考えてみましょう。
※デストラクタ以外のvirtual関数はちょっと動きが違うので、別途説明します。
(デストラクタ以外は動作確認ですぐ発見できますが、デストラクタはやばいので先に実験です。)
簡単に回答するなら、
継承という機能を有効に使うなら、virtualを付けてあげるべきです。
厳密には色々ありますが、SAIが規約を作るなら、
ベースクラスのデストラクタにはvirtualを付けろ!
です。
ただ、おそらく理由を理解しないとすぐ忘れます。
では、ベースクラスの関数、特にデストラクタにvirtualなければどうなるか、
難しいことは後回しにして、
実際どうなるかを見てみたほうが良いですね。
---------------------------------
/** ベースクラス
* コンストラクタで、ベースクラスデバック用の13番PinをON、
* デストラクタで、ベースクラスデバック用の13番PinをOFF
*/
class LedBase
{
public:
LedBase();
~LedBase(); // ★virtualなし!
};
/** 派生クラス
* コンストラクタで、子クラスデバック用の12番PinをON、
* デストラクタで、ベースクラスデバック用の12番PinをOFF
*/
class LedControl :public LedBase
{
public:
LedControl();
~LedControl();
};
---------------------------------
このクラス構成で、LedControl をnewおよびdeleteしたらどうなるでしょうか?
単純に考えたら、
子クラスをNewしたら、13番Pinが光って、12番ピンが光り、
子クラスをdeleteしたら、12番Pinが消えて、13番ピンが消えます。
これを以下のような、子クラスのインスタンスとして制御をするとどうなるでしょうか?
/* 子クラスとして制御 */
LedControl* lControl = new LedControl();
delay(1000);
delete lControl;
lControl = NULL;
delay(2000);
↑new ↑delete
newしたとき
①親クラスのコンストラクタ
②子クラスのコンストラクタ
deleteしたとき
③子クラスのデストラクタ
④親クラスのデストラクタ
の順で呼び出されました。
いいですね。
さて、
ベースクラスを作るということは、派生クラスを沢山作ることになります。
ボタンAの子クラス、ボタンBの子クラスといったっ感じ。
ただ、実処理としては、
呼び出しをする側は、これらを各子クラスとして呼び出しをするのは
処理的にも単調で美しくないです。
仮に各子クラスをA~Dとすると以下のような形になります。
生成するときは
LedControlA* lControlA = new LedControlA();
LedControlB* lControlB = new LedControlB();
LedControlC* lControlC = new LedControlC();
LedControlD* lControlD = new LedControlD();
機能を使うときは
lControlA ->Func();
lControlB->Func();
lControlC->Func();
lControlD->Func();
deleteするときは、
delete lControlA;
delete lControlB;
delete lControlC;
delete lControlD;
それぞれ一つ一つ呼ばなくてはならず、美しくないです。
ではどう制御するか?
ベースクラスの制御する側は基本的に親クラス型で管理するほうが、
美しく可読性もいいです。
また、設計変更により機能を追加した場合にも、
修正箇所が生成処理の部分を追加すればOKで、
機能関数の呼び出し箇所に対しては、汎用処理なら
修正しなくても既存の処理のままでOKとなります。
生成するときは
const int ClassMax =4;
LedBase* lLedList[ClassMax];
lLedList[0] = new LedControlA();
lLedList[1] = new LedControlB();
lLedList[2] = new LedControlC();
lLedList[3] = new LedControlC();
機能を使うときは
for(int i = 0 ; ClassMax >i ; i++)
{
lLedList[i]->Func();
}
deleteするときは、
for(int i = 0 ; ClassMax >i ; i++)
{
delete lLedList[i];
lLedList[i] = null;
}
こんな感じです。
さて、ここで問題になるのが、deleteしているのは親クラス型であること。
先ほどのクラスを、親クラス型として制御をするとどうなるでしょうか?
/* 次は、親クラスでコントロールする */
LedBase* lBase = new LedControl();
delay(1000);
delete lBase ;
lBase = NULL;
delay(5000);
どうなったでしょうか?
↑new ↑delete
newしたとき
①親クラスのコンストラクタ
②子クラスのコンストラクタ
deleteしたとき
③子クラスのデストラクタはよびだされません!
④親クラスのデストラクタ
の順で呼び出されました。
はい、③子クラスの解放漏れ発生です。
では、もしベースクラスのデストラクタにvirtualの記載があると、
どうなるでしょうか?
先ほどのクラス宣言の
// ★virtualなし!
のところに、virtualを付けてみましょう。
↑new ↑delete
newしたとき
①親クラスのコンストラクタ
②子クラスのコンストラクタ
deleteしたとき
③子クラスのデストラクタ
④親クラスのデストラクタ
の順で呼び出されました。
というわけで、子クラスにはvirtualが必要になるわけです。
では、どういう仕組みでこんな動作になるのでしょうか?
ここで、クラスの構成を理解する必要が出てきます。
クラス図にするとこんな感じですね。
最初に行ったように子クラス型で呼び出しを行うと、
4つの関数すべてが見えています。
ちょっと極端なイメージですが、以下のような関数軍があるような感じです。

(イメージです)
ところが、親クラス型で呼び出しを行う場合、
子クラスの関数が見えません。
ま、includeを考えたらわかりそうなもんですが、
Baseで操作するクラスはBaseクラスしかIncludeしていないので、
派生クラスにどんな関数が追加されているかなんて知ったこっちゃないんです。
しかし、そんなことを言い出したら、
子クラスの関数を呼び出せないじゃん!
継承を使う意味ないじゃん
ということになります。
そこで活躍するのが、virtualです。
SAIの理解では、virtualというマークがあれば、
子クラスに派生した関数があるかもしれないよ!
というキーワードになります。
つまり、
(イメージです)
こんな感じ。
デストラクタが呼び出されたら、
子クラスにもデストラクタがあるかもしれない!
ということになるわけです。
#ただ、通常のvirtual関数と違い、ベースクラスのデストラクタも呼び出されるところが特殊ですね。
じゃぁ、ベースクラスは全部の関数にvirtualを付けたらいいじゃん!
とはなりません。
virtualを付けると、
派生クラスに関数があるかも!ということで派生クラスから関数を探す処理?
みたいなのが挟まるらしく、
ちょっと処理コストが高いらしいです。
以下二つが成立したときのみvirtualをつけるのが本当は正しいのだとおもいます。
①オーバーライドしていい関数+デストラクタ
②派生クラスを使う人が、ベースクラス型で呼び出す可能性がある
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
雑談
とはいうものの、実際のところはうっかりさんがいるものです。
virtualがついていないのに、ベースクラス型で操作したり、
とある関数のみvirtualを付けるのを忘れたり、
という凡ミスで不具合を発生させることがありますので、
ベースクラスで、派生する可能性のある関数は全部virtualつけるという規約の方が、
不具合が起きにくいのかもしれません。
そしてもう一つ、
コンストラクタにvirtualは必要ないのか?
→必要ありません。
だって、コンストラクタってクラス生成時しか呼び出されませんよね。
では、クラスの生成を親クラスの型で行なわれたらどうなるか?
それはもはや子クラスではないです。
というわけで、親クラス型で子クラスを呼ぶようなことはあり得ないんで、不要となるものと考えています。
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
SAIでした。
本日使ったコードは以下の通りです!
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
const int CBASE_PIN_ID = 13;
const int CCHILD_PIN_ID = 12;
/** ベースクラス */
class LedBase
{
public:
LedBase();
virtual ~LedBase(); // virtualあり!
// ~LedBase(); // virtualなし!
};
/** 派生クラス */
class LedControl :public LedBase
{
public:
LedControl();
~LedControl();
};
/** ベースクラスのコンストラクタ */
LedBase::LedBase()
{
delay(100);
// 出力設定 //
pinMode(CBASE_PIN_ID, OUTPUT);
// ベースクラスの生命PIN制御 //
digitalWrite(CBASE_PIN_ID, HIGH); // IO OFF
}
/** ベースクラスのデストラクタ */
LedBase::~LedBase()
{
delay(100);
// ベースクラスの生命PIN制御 //
digitalWrite(CBASE_PIN_ID, LOW); // IO OFF
}
/** 子クラスのコンストラクタ */
LedControl::LedControl()
{
delay(100);
// 出力設定 //
pinMode(CCHILD_PIN_ID, OUTPUT);
// ベースクラスの生命PIN制御 //
digitalWrite(CCHILD_PIN_ID, HIGH); // IO OFF
}
/** 子クラスのデストラクタ */
LedControl::~LedControl()
{
delay(100);
// ベースクラスの生命PIN制御 //
digitalWrite(CCHILD_PIN_ID, LOW); // IO OFF
}
/* Loop関数 */
void loop() {
// put your main code here, to run repeatedly:
/* 最初は、子クラスでコントロールする */
/*
LedControl* lControl = new LedControl();
delay(1000);
delete lControl;
lControl = NULL;
delay(2000);
*/
/* 次は、親クラスでコントロールする */
LedBase* lBase = new LedControl();
delay(1000);
delete lBase ;
lBase = NULL;
delay(5000);
}
/* setup関数 */
void setup() {
// put your setup code here, to run once:
}





