Cpp クラス 仮想関数

提供:yonewiki

C++に戻る


本来の表記は「C++(Cpp) クラス 仮想関数」です。この記事に付けられた題名はテンプレート:記事名の制約から不正確なものとなっています。

※このページではC++にのみ存在する機能として、記事タイトルがC++ クラス 仮想関数になっています。

クラス 仮想関数

 前の項目のCpp クラス 継承 アップキャストで、デストラクタが呼ばれず、プログラムがメモリリークを起こすような怪しい終わり方になっていました。その対策方法を「継承 デストラクタ」の項目で説明するつもりなのですが、対策をするには、この仮想関数という仕組みを理解する必要があります。


 ざっくり言うと、基底クラス側で、派生クラスでも同じ名前の関数を作ってもいいよ。っていうメカニズムです。今回作った継承のサンプルだと基底クラスの機能があまりにも少ないので、基底クラスに仮想関数を設定しにくいので、もう少し肉付けしてみます。


 いい感じのサンプルになるかな?例えば、これまでに作ったプログラムでは、アップキャストしても基底クラスで保有している関数しか使えないため、基本料金の合計(月額料金×利用月数)と全体の合計金額しか表示することが出来ていませんでした。オプション料金が発生したときは、ちゃんとオプション料金がいくらだったのかを表示してほしいとします。そうすると、「基本料金、合計金額」という出力ではなく、「基本料金、オプション明細、合計金額」というような出力をする関数が欲しいところです。この仕組みを実現するために、まずは基底クラスにvoid mfVirtual_vDispValue();という関数を追加します。


cpp (基底クラス BaseInheritance.h)

#ifndef __BASEINHERITANCE_H_YONET__
#define __BASEINHERITANCE_H_YONET__
#if _MSC_VER > 1000
#pragma once
#endif

class CBaseInheritance {
protected:
    int m_iBaseMoney = 0;
    int m_iBaseMonth = 0;
    int m_iMoney = 0;
    int mf_iBaseSumMoney();
public:
    CBaseInheritance();
    CBaseInheritance(int iArgBaseMoney, int iArgBaseMonth);
    ~CBaseInheritance();
    void mfVirtual_vDispValue();
    void mf_vBaseDispValue();
};

 で、まずは、基底クラスとしての、オプション明細つきとして、Option="None"と表示する以下のようなプログラムとしてみます。

cpp (基底クラス BaseInheritance.cpp)

#include "pch.h"
#include "BaseInheritance.h"

CBaseInheritance::CBaseInheritance() {
    printf("Constructor:CBaseInheritance()\n");
}

CBaseInheritance::CBaseInheritance(int iArgBaseMoney, int iArgBaseMonth)
                 :m_iBaseMoney(iArgBaseMoney)
                 ,m_iBaseMonth(iArgBaseMonth)
                 ,m_iMoney(0) {
    printf("Constructor:CBaseInheritance(int,int)\n");
}

CBaseInheritance::~CBaseInheritance() {
    printf("Destructor:~CBaseInheritance()\n");
}

int CBaseInheritance::mf_iBaseSumMoney() {
    m_iMoney = m_iBaseMoney * m_iBaseMonth;
    return m_iMoney;
}

void CBaseInheritance::mfVirtual_vDispValue() {
    int iSumMoney = mf_iBaseSumMoney();
    printf("BaseMoney=%d, option=\"none\" total=%d\n", iSumMoney,m_iMoney);
}

void CBaseInheritance::mf_vBaseDispValue() {
    int iSumMoney = m_iBaseMoney * m_iBaseMonth;
    printf("BaseMoney=%d, total=%d\n", iSumMoney, m_iMoney);
}

 これで以下のようなメイン関数を作って

cpp (メイン関数 InheritanceMain.cpp)

#include "pch.h"
#include "BaseInheritance.h"
#include "DeriveInheritance.h"
#include "DeriveArrInheritance.h"
#include "DeriveDiscountInheritance.h"

CBaseInheritance* pCBaseInheritanceUpcastF(int iSelectDerive) {

    CDeriveInheritance* pCDeriveInheitance;
    pCDeriveInheitance = new CDeriveInheritance(1900, 12);
    pCDeriveInheitance->mf_vDeriveSetOptionValue(900);
    return pCDeriveInheitance;
};

int main(int iArgSize, char* pcArgArr[]) {
 
    CBaseInheritance objCBaseIneritance(1900, 12);
    objCBaseIneritance.mfVirtual_vDispValue();

    CBaseInheritance* pCBaseInheritanceUpcast;
    pCBaseInheritanceUpcast = pCBaseInheritanceUpcastF(1);
    pCBaseInheritanceUpcast->mfVirtual_vDispValue();
    delete pCBaseInheritanceUpcast;

    return 0;

}

 実行すると

実行結果

Constructor:CBaseInheritance(int,int)
BaseMoney=22800, option="none" total=22800
Destructor:~CBaseInheritance()

 と、なってオプション明細表示機能を基底クラスに持たせることができました。この関数を派生クラスでも同じように持たせれば、同じ機能を作れるのですが、同じ関数がある場合でも、派生クラスの方の関数が呼ばれて欲しいところです。同じ機能だから同じ名前を使うというのは、やっていいことです。ですが、アップキャストされた場合は、基底クラスの方にある関数を呼び出してしまいます。元の機能を優先して呼び出してほしいです。派生クラスの関数なんて基本クラスは知る由もないはずですが、そのジレンマみたいなものを解決するのが、仮想関数という機能です。


 ちなみにアップキャストの記事で使ったプログラムのメイン関数だけを以下のように替えて確かめるとすると、


cpp (メイン関数 InheritanceMain.cpp)

#include "pch.h"
#include "BaseInheritance.h"
#include "DeriveInheritance.h"
#include "DeriveArrInheritance.h"
#include "DeriveDiscountInheritance.h"

CBaseInheritance* pCBaseInheritanceUpcastF(int iSelectDerive) {

    CDeriveInheritance* pCDeriveInheitance;
    pCDeriveInheitance = new CDeriveInheritance(1900, 12);
    pCDeriveInheitance->mf_vDeriveSetOptionValue(900);
    return pCDeriveInheitance;
};

int main(int iArgSize, char* pcArgArr[]) {
 
    CDeriveInheritance objCDeriveIneritance(1900, 12, 900);
    objCDeriveIneritance.mfVirtual_vDispValue();

    CBaseInheritance* pCBaseInheritanceUpcast;
    pCBaseInheritanceUpcast = pCBaseInheritanceUpcastF(1);
    pCBaseInheritanceUpcast->mfVirtual_vDispValue();
    delete pCBaseInheritanceUpcast;

    return 0;

}

 となって、実行結果は

実行結果

Constructor:CBaseInheritance(int,int)
Constructor:CDriveInheritance(int,int,int)
BaseMoney=22800, OptionMoney=600 total=23400
Constructor:CBaseInheritance(int,int)
Constructor:CDriveInheritance(int,int,int)
BaseMoney=22800, option="none" total=23400
Destructor:~CDriveInheritance()
Destructor:~CBaseInheritance()

 という具合です。アップキャストした方が派生クラスの関数がわからないので、基底クラスの方の関数が呼ばれています。オプションがいくらかあって、基本料金と合計金額に差があるのに、optionは知らんみたいになって残念な感じです。で、これを解決するのが、仮想関数だというキーワードを出していました。仮想関数という機能を使うには基底クラスが仮想関数の存在を認める宣言をするだけです。


 具体的に以下のような基底クラスのヘッダファイル宣言にするだけです。

cpp (基底クラス BaseInheritance.h)

#ifndef __BASEINHERITANCE_H_YONET__
#define __BASEINHERITANCE_H_YONET__
#if _MSC_VER > 1000
#pragma once
#endif

class CBaseInheritance {
protected:
    int m_iBaseMoney = 0;
    int m_iBaseMonth = 0;
    int m_iMoney = 0;
    int mf_iBaseSumMoney();
public:
    CBaseInheritance();
    CBaseInheritance(int iArgBaseMoney, int iArgBaseMonth);
    ~CBaseInheritance();
    virtual void mfVirtual_vDispValue();
    void mf_vBaseDispValue();
};
#endif

 617行目の

    virtual void mfVirtual_vDispValue();

 ようにvirtualというキーワードを宣言の部分に書けば、仮想関数という扱いになり、基底クラスでは、存在しないかもしれない架空の関数としての振る舞いになり、アップキャストされている場合は派生クラスに同じ名前の関数があるはずだから、それを見に行くようになります。この存在しないかもしれない架空の扱いを仮想関数と呼んでいるのだと思います。基底クラスでは、ある機能が自分自身にも派生クラスにもあるべきだという意味で使われます。今回の場合は、それぞれのクラス、基底クラス、派生クラスの全てに独自の合計金額を出力する関数があって、それぞれで動きは違うけど、同じ目的をもった関数が作られています。仮想関数はこういう使い方をします。実際のプログラミングでは、このサンプルよりも驚くべき使われ方や画期的な使われ方がされていることの方が多いです。


 そうすると、virtualをつけた場合の動作は

実行結果

Constructor:CBaseInheritance(int,int)
Constructor:CDriveInheritance(int,int,int)
BaseMoney=22800, OptionMoney=600 total=23400
Constructor:CBaseInheritance(int,int)
Constructor:CDriveInheritance(int,int,int)
BaseMoney=22800, OptionMoney=600 total=23400
Destructor:~CBaseInheritance()
Destructor:~CDriveInheritance()
Destructor:~CBaseInheritance()


となります。が、ここでも、まだ、デストラクタの数がコンストラクタの数と一致せず、まだまだ残念な感じになっています。


 でも、これで、継承 デストラクタの記事への下準備ができました。答えはもうわかってると思いますけど…


C++に戻る